Track recently active and signin times (#663)
* Track recently active and signin times * Trust proxy headers in production
This commit is contained in:
@ -86,6 +86,9 @@ router.get('google.callback', async ctx => {
|
|||||||
await team.createFirstCollection(user.id);
|
await team.createFirstCollection(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not awaiting the promise here so that the request is not blocked
|
||||||
|
user.updateSignedIn(ctx.request.ip);
|
||||||
|
|
||||||
ctx.cookies.set('lastSignedIn', 'google', {
|
ctx.cookies.set('lastSignedIn', 'google', {
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
expires: new Date('2100'),
|
expires: new Date('2100'),
|
||||||
|
@ -62,6 +62,9 @@ router.get('slack.callback', async ctx => {
|
|||||||
await team.createFirstCollection(user.id);
|
await team.createFirstCollection(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not awaiting the promise here so that the request is not blocked
|
||||||
|
user.updateSignedIn(ctx.request.ip);
|
||||||
|
|
||||||
ctx.cookies.set('lastSignedIn', 'slack', {
|
ctx.cookies.set('lastSignedIn', 'slack', {
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
expires: new Date('2100'),
|
expires: new Date('2100'),
|
||||||
|
@ -70,6 +70,10 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
|
|
||||||
app.use(mount('/emails', emails));
|
app.use(mount('/emails', emails));
|
||||||
} else if (process.env.NODE_ENV === 'production') {
|
} else if (process.env.NODE_ENV === 'production') {
|
||||||
|
// trust header fields set by our proxy. eg X-Forwarded-For
|
||||||
|
app.proxy = true;
|
||||||
|
|
||||||
|
// catch errors in one place, automatically set status and response headers
|
||||||
onerror(app);
|
onerror(app);
|
||||||
|
|
||||||
if (process.env.BUGSNAG_KEY) {
|
if (process.env.BUGSNAG_KEY) {
|
||||||
|
@ -79,6 +79,9 @@ export default function auth(options?: { required?: boolean } = {}) {
|
|||||||
throw new UserSuspendedError({ adminEmail: suspendingAdmin.email });
|
throw new UserSuspendedError({ adminEmail: suspendingAdmin.email });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not awaiting the promise here so that the request is not blocked
|
||||||
|
user.updateActiveAt(ctx.request.ip);
|
||||||
|
|
||||||
ctx.state.token = token;
|
ctx.state.token = token;
|
||||||
ctx.state.user = user;
|
ctx.state.user = user;
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
|
26
server/migrations/20180604182823-user-tracking.js
Normal file
26
server/migrations/20180604182823-user-tracking.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('users', 'lastActiveAt', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('users', 'lastActiveIp', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('users', 'lastSignedInAt', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('users', 'lastSignedInIp', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('users', 'lastActiveAt');
|
||||||
|
await queryInterface.removeColumn('users', 'lastActiveIp');
|
||||||
|
await queryInterface.removeColumn('users', 'lastSignedInAt');
|
||||||
|
await queryInterface.removeColumn('users', 'lastSignedInIp');
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import crypto from 'crypto';
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import JWT from 'jsonwebtoken';
|
import JWT from 'jsonwebtoken';
|
||||||
|
import subMinutes from 'date-fns/sub_minutes';
|
||||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||||
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
||||||
import { sendEmail } from '../mailer';
|
import { sendEmail } from '../mailer';
|
||||||
@ -28,6 +29,10 @@ const User = sequelize.define(
|
|||||||
serviceId: { type: DataTypes.STRING, allowNull: true, unique: true },
|
serviceId: { type: DataTypes.STRING, allowNull: true, unique: true },
|
||||||
slackData: DataTypes.JSONB,
|
slackData: DataTypes.JSONB,
|
||||||
jwtSecret: encryptedFields.vault('jwtSecret'),
|
jwtSecret: encryptedFields.vault('jwtSecret'),
|
||||||
|
lastActiveAt: DataTypes.DATE,
|
||||||
|
lastActiveIp: DataTypes.STRING,
|
||||||
|
lastSignedInAt: DataTypes.DATE,
|
||||||
|
lastSignedInIp: DataTypes.STRING,
|
||||||
suspendedAt: DataTypes.DATE,
|
suspendedAt: DataTypes.DATE,
|
||||||
suspendedById: DataTypes.UUID,
|
suspendedById: DataTypes.UUID,
|
||||||
},
|
},
|
||||||
@ -53,6 +58,24 @@ User.associate = models => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Instance methods
|
// Instance methods
|
||||||
|
User.prototype.updateActiveAt = function(ip) {
|
||||||
|
const fiveMinutesAgo = subMinutes(new Date(), 5);
|
||||||
|
|
||||||
|
// ensure this is updated only every few minutes otherwise
|
||||||
|
// we'll be constantly writing to the DB as API requests happen
|
||||||
|
if (this.lastActiveAt < fiveMinutesAgo) {
|
||||||
|
this.lastActiveAt = new Date();
|
||||||
|
this.lastActiveIp = ip;
|
||||||
|
return this.save({ hooks: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.updateSignedIn = function(ip) {
|
||||||
|
this.lastSignedInAt = new Date();
|
||||||
|
this.lastSignedInIp = ip;
|
||||||
|
return this.save({ hooks: false });
|
||||||
|
};
|
||||||
|
|
||||||
User.prototype.getJwtToken = function() {
|
User.prototype.getJwtToken = function() {
|
||||||
return JWT.sign({ id: this.id }, this.jwtSecret);
|
return JWT.sign({ id: this.id }, this.jwtSecret);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user