Skip to content

Commit

Permalink
feat(general): access & refresh tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Peshkov committed Jan 31, 2020
1 parent 0bbdd06 commit d78a631
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 56 deletions.
3 changes: 2 additions & 1 deletion config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const config = {
media_url: '/frontend/media',
salt_rounds: 12,
ttl: {
access_token: 10 * 60 // 10 minutes
access_token: 10 * 60, // 10 minutes
mail_confirmation: 48 * 60 * 60 // 48 hours
},
filter_fields: [
'token',
Expand Down
4 changes: 2 additions & 2 deletions lib/cron.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { Sequelize } = require('./sequelize');


const JobCallbacks = {
DELETE_NOT_CONFIRMED_USERS: async ({ id }) => {
DELETE_NOT_CONFIRMED_USERS: async () => {
const confirmations = await MailConfirmation.findAll({
where: {
expires_at: { [Sequelize.Op.lt]: new Date() }
Expand Down Expand Up @@ -46,7 +46,7 @@ class JobManager {
DELETE_NOT_CONFIRMED_USERS: {
key: 'DELETE_NOT_CONFIRMED_USERS',
description: 'Deleting mail confirmations and their users.',
rule: '*/10 * * * *',
rule: '*/1 * * * *',
callback: JobCallbacks.DELETE_NOT_CONFIRMED_USERS
}
};
Expand Down
32 changes: 15 additions & 17 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
function filterFields (body, fieldsToFilter) {
const flatten = flattenObject(body);
for (const field in flatten) {
if (fieldsToFilter.some((filterField) => field === filterField)) {
flatten[field] = '[FILTERED]';
}
}

return unflattenObject(flatten);
};

// A helper to flatten the nested object. Copypasted from Google.
function flattenObject (obj, prefix = '') {
function flattenObject(obj, prefix = '') {
return Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
if (typeof obj[k] === 'object' && obj[k] !== null && Object.prototype.toString.call(obj[k]) !== '[object Date]') {
Expand All @@ -21,10 +10,9 @@ function flattenObject (obj, prefix = '') {

return acc;
}, {});
};

}

function unflattenObject (data) {
function unflattenObject(data) {
const result = {};

for (const i in data) {
Expand All @@ -34,11 +22,21 @@ function unflattenObject (data) {
}, result);
}
return result;
};
}

function filterFields(body, fieldsToFilter) {
const flatten = flattenObject(body);
for (const field in flatten) {
if (fieldsToFilter.some((filterField) => field === filterField)) {
flatten[field] = '[FILTERED]';
}
}

return unflattenObject(flatten);
}

module.exports = {
filterFields,
flattenObject,
unflattenObject
}
};
2 changes: 1 addition & 1 deletion lib/morgan.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ module.exports = morgan((tokens, req, res) => {
}

return result;
}, { stream: log.stream });
}, { stream: log.stream });
2 changes: 2 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const cron = require('./cron');

const middlewares = require('../middlewares/generic');
const campaigns = require('../middlewares/campaigns');
const register = require('../middlewares/register');

const GeneralRouter = router({ mergeParams: true });

Expand All @@ -32,6 +33,7 @@ process.on('unhandledRejection', (err) => {

GeneralRouter.get('/healthcheck', middlewares.healthcheck);
GeneralRouter.post('/signup/:campaign_id', campaigns.registerUser);
GeneralRouter.post('/confirm-email', register.confirmEmail);

server.use('/', GeneralRouter);

Expand Down
4 changes: 2 additions & 2 deletions middlewares/campaigns.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ module.exports.registerUser = async (req, res) => {

return res.json({
success: true,
data: {
data: {
user,
confirmation
}
})
});
};
62 changes: 31 additions & 31 deletions middlewares/generic.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
const errors = require('../lib/errors');
const logger = require('../lib/logger');
// const { User, AccessToken } = require('../models');
const { User, AccessToken } = require('../models');

const packageInfo = require('../package');

// exports.maybeAuthorize = async (req, res, next) => {
// const authToken = req.headers['x-auth-token'];
// if (!authToken) {
// return next();
// }
exports.maybeAuthorize = async (req, res, next) => {
const authToken = req.headers['x-auth-token'];
if (!authToken) {
return next();
}

// const accessToken = await AccessToken.findOne({
// where: {
// value: authToken,
// },
// include: [User]
// });
const accessToken = await AccessToken.findOne({
where: {
value: authToken,
},
include: [User]
});

// if (!accessToken) {
// return next();
// }
if (!accessToken) {
return next();
}

// if (moment(accessToken.expires_at).isBefore(moment())) {
// logger.debug('Access token is expired');
// return next();
// }
if (moment(accessToken.expires_at).isBefore(moment())) {
logger.debug('Access token is expired');
return next();
}

// req.user = accessToken.user;
req.user = accessToken.user;

// return next();
// };
return next();
};

// exports.ensureAuthorized = async (req, res, next) => {
// if (!req.user) {
// return res.status(401).json({
// success: false,
// message: 'You are not authorized.'
// });
// }
exports.ensureAuthorized = async (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'You are not authorized.'
});
}

// return next();
// };
return next();
};

/* istanbul ignore next */
exports.healthcheck = (req, res) => {
Expand Down
57 changes: 57 additions & 0 deletions middlewares/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { User, AccessToken, RefreshToken } = require('../models');
const { Sequelize } = require('../lib/sequelize');
const errors = require('../lib/errors');

module.exports.login = async (req, res) => {
const user = await User.scope('withPassword').findOne({
[Sequelize.Op.or]: {
email: req.body.login,
username: req.body.login
}
});

if (!user) {
return errors.makeNotFoundError(res, 'User is not found.');
}

if (!await user.checkPassword(req.body.password)) {
return errors.makeUnauthorizedError(res, 'Password is not valid.');
}

if (!user.mail_confirmed_at) {
return errors.makeUnauthorizedError(res, 'Please confirm your mail first.');
}

const accessToken = await AccessToken.createForUser(user.id);
const refreshToken = await RefreshToken.createForUser(user.id);

return res.json({
success: true,
data: {
access_token: accessToken.value,
refresh_token: refreshToken.value
}
});
};

module.exports.renew = async (req, res) => {
const token = await RefreshToken.findOne({
value: req.body.token
});

if (!token) {
return res.status(401).json({
success: false,
message: 'Token is not found.'
});
}

const accessToken = await AccessToken.createForUser(token.user_id);

return res.json({
success: true,
data: {
access_token: accessToken.value,
}
});
};
30 changes: 30 additions & 0 deletions middlewares/register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { MailConfirmation, User } = require('../models');
const errors = require('../lib/errors');


module.exports.confirmEmail = async (req, res) => {
if (!req.body.token) {
return errors.makeBadRequestError(res, 'No token specified.');
}

const confirmation = await MailConfirmation.findOne({
where: { value: req.body.token },
include: [User]
});

if (!confirmation) {
return errors.makeNotFoundError(res, 'The token is invalid.');
}

if (confirmation.is_expired) {
return errors.makeForbiddenError(res, 'The token has expired.');
}

await confirmation.user.update({ mail_confirmed_at: new Date() });
await confirmation.destroy();

return res.json({
success: true,
message: 'Your profile is activated.'
});
};
31 changes: 31 additions & 0 deletions migrations/20200131145144-create-refresh-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('refresh_tokens', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
value: {
type: Sequelize.TEXT,
allowNull: false
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: (queryInterface) => queryInterface.dropTable('refresh_tokens')
};
35 changes: 35 additions & 0 deletions migrations/20200131145244-create-access-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('access_tokens', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
value: {
type: Sequelize.TEXT,
allowNull: false
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
},
expires_at: {
allowNull: false,
type: Sequelize.DATE
},
}),
down: (queryInterface) => queryInterface.dropTable('access_tokens')
};
46 changes: 46 additions & 0 deletions models/AccessToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const crypto = require('crypto');
const util = require('util');
const moment = require('moment');

const { Sequelize, sequelize } = require('../lib/sequelize');
const config = require('../config');

const randomBytes = util.promisify(crypto.randomBytes);

const AccessToken = sequelize.define('access_token', {
user_id: {
type: Sequelize.INTEGER,
allowNull: false
},
value: {
type: Sequelize.TEXT,
allowNull: false,
defaultValue: '',
validate: {
notEmpty: { msg: 'Value should be set.' },
},
unique: true
},
expires_at: {
type: Sequelize.DATE,
allowNull: false
}
}, {
underscored: true,
tableName: 'access_tokens',
createdAt: 'created_at',
updatedAt: 'updated_at'
});

AccessToken.createForUser = async function createForUser(userId) {
const value = (await randomBytes(64)).toString('hex');
const expiresAt = moment().add(config.ttl.access_token, 'seconds');

return AccessToken.create({
user_id: userId,
value,
expires_at: expiresAt
});
};

module.exports = AccessToken;
Loading

0 comments on commit d78a631

Please sign in to comment.