1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-24 15:49:46 +02:00

feat: Invalidate access token on logout

This commit is contained in:
Maksim Eltyshev 2022-09-07 18:39:33 +05:00
parent 640908320a
commit 8109936ce2
26 changed files with 242 additions and 37 deletions

View file

@ -40,24 +40,33 @@ module.exports = {
},
async fn(inputs) {
const remoteAddress = getRemoteAddress(this.req);
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) {
sails.log.warn(
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
this.req,
)})`,
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
);
throw Errors.INVALID_EMAIL_OR_USERNAME;
}
if (!bcrypt.compareSync(inputs.password, user.password)) {
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
throw Errors.INVALID_PASSWORD;
}
const accessToken = sails.helpers.utils.createToken(user.id);
await Session.create({
accessToken,
remoteAddress,
userId: user.id,
userAgent: this.req.headers['user-agent'],
});
return {
item: sails.helpers.utils.createToken(user.id),
item: accessToken,
};
},
};

View file

@ -0,0 +1,16 @@
module.exports = {
async fn() {
const { accessToken } = this.req;
await Session.updateOne({
accessToken,
deletedAt: null,
}).set({
deletedAt: new Date().toUTCString(),
});
return {
item: accessToken,
};
},
};

View file

@ -1,6 +1,8 @@
const bcrypt = require('bcrypt');
const zxcvbn = require('zxcvbn');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
USER_NOT_FOUND: {
userNotFound: 'User not found',
@ -71,6 +73,13 @@ module.exports = {
if (user.id === currentUser.id) {
const accessToken = sails.helpers.utils.createToken(user.id, user.passwordUpdatedAt);
await Session.create({
accessToken,
userId: user.id,
remoteAddress: getRemoteAddress(this.req),
userAgent: this.req.headers['user-agent'],
});
return {
item: user,
included: {

View file

@ -1,3 +1,4 @@
const { v4: uuid } = require('uuid');
const jwt = require('jsonwebtoken');
module.exports = {
@ -24,6 +25,9 @@ module.exports = {
exp: iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60,
},
sails.config.session.secret,
{
keyid: uuid(),
},
);
},
};

View file

@ -17,6 +17,15 @@ module.exports = function defineCurrentUserHook(sails) {
return null;
}
const session = await Session.findOne({
accessToken,
deletedAt: null,
});
if (!session) {
return null;
}
const user = await sails.helpers.users.getOne(payload.subject);
if (user && user.passwordChangedAt > payload.issuedAt) {
@ -43,8 +52,14 @@ module.exports = function defineCurrentUserHook(sails) {
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) {
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, '');
const currentUser = await getUser(accessToken);
req.currentUser = await getUser(accessToken);
if (currentUser) {
Object.assign(req, {
accessToken,
currentUser,
});
}
}
return next();
@ -52,8 +67,17 @@ module.exports = function defineCurrentUserHook(sails) {
},
'/attachments/*': {
async fn(req, res, next) {
if (req.cookies.accessToken) {
req.currentUser = await getUser(req.cookies.accessToken);
const { accessToken } = req.cookies;
if (accessToken) {
const currentUser = await getUser(accessToken);
if (currentUser) {
Object.assign(req, {
accessToken,
currentUser,
});
}
}
return next();

49
server/api/models/Session.js Executable file
View file

@ -0,0 +1,49 @@
/**
* Session.js
*
* @description :: A model definition represents a database table/collection.
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/
module.exports = {
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
accessToken: {
type: 'string',
required: true,
columnName: 'access_token',
},
remoteAddress: {
type: 'string',
required: true,
columnName: 'remote_address',
},
userAgent: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
columnName: 'user_agent',
},
deletedAt: {
type: 'ref',
columnName: 'deleted_at',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
userId: {
model: 'User',
required: true,
columnName: 'user_id',
},
},
};

View file

@ -10,6 +10,7 @@
module.exports.routes = {
'POST /api/access-tokens': 'access-tokens/create',
'DELETE /api/access-tokens/me': 'access-tokens/delete',
'GET /api/users': 'users/index',
'POST /api/users': 'users/create',

View file

@ -0,0 +1,24 @@
module.exports.up = (knex) =>
knex.schema.createTable('session', (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
table.bigInteger('user_id').notNullable();
table.text('access_token').notNullable();
table.text('remote_address').notNullable();
table.text('user_agent');
table.timestamp('created_at', true);
table.timestamp('updated_at', true);
table.timestamp('deleted_at', true);
/* Indexes */
table.index('user_id');
table.unique('access_token');
table.index('remote_address');
});
module.exports.down = (knex) => knex.schema.dropTable('session');