From 53a0f423c38fe8968bcc03ee69d6a919b5303f2d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 4 Jun 2018 19:07:56 -0700 Subject: [PATCH] Track recently active and signin times (#663) * Track recently active and signin times * Trust proxy headers in production --- server/auth/google.js | 3 +++ server/auth/slack.js | 3 +++ server/index.js | 4 +++ server/middlewares/authentication.js | 3 +++ .../20180604182823-user-tracking.js | 26 +++++++++++++++++++ server/models/User.js | 23 ++++++++++++++++ 6 files changed, 62 insertions(+) create mode 100644 server/migrations/20180604182823-user-tracking.js diff --git a/server/auth/google.js b/server/auth/google.js index 09145818..7553fe52 100644 --- a/server/auth/google.js +++ b/server/auth/google.js @@ -86,6 +86,9 @@ router.get('google.callback', async ctx => { 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', { httpOnly: false, expires: new Date('2100'), diff --git a/server/auth/slack.js b/server/auth/slack.js index 115c0349..e6f3279c 100644 --- a/server/auth/slack.js +++ b/server/auth/slack.js @@ -62,6 +62,9 @@ router.get('slack.callback', async ctx => { 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', { httpOnly: false, expires: new Date('2100'), diff --git a/server/index.js b/server/index.js index d547d637..4439c4c6 100644 --- a/server/index.js +++ b/server/index.js @@ -70,6 +70,10 @@ if (process.env.NODE_ENV === 'development') { app.use(mount('/emails', emails)); } 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); if (process.env.BUGSNAG_KEY) { diff --git a/server/middlewares/authentication.js b/server/middlewares/authentication.js index 11f3717c..59a0e577 100644 --- a/server/middlewares/authentication.js +++ b/server/middlewares/authentication.js @@ -79,6 +79,9 @@ export default function auth(options?: { required?: boolean } = {}) { 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.user = user; // $FlowFixMe diff --git a/server/migrations/20180604182823-user-tracking.js b/server/migrations/20180604182823-user-tracking.js new file mode 100644 index 00000000..fe0b3ed1 --- /dev/null +++ b/server/migrations/20180604182823-user-tracking.js @@ -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'); + } +} \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js index 96bd49e8..ff3b1a6e 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -3,6 +3,7 @@ import crypto from 'crypto'; import bcrypt from 'bcrypt'; import uuid from 'uuid'; import JWT from 'jsonwebtoken'; +import subMinutes from 'date-fns/sub_minutes'; import { DataTypes, sequelize, encryptedFields } from '../sequelize'; import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3'; import { sendEmail } from '../mailer'; @@ -28,6 +29,10 @@ const User = sequelize.define( serviceId: { type: DataTypes.STRING, allowNull: true, unique: true }, slackData: DataTypes.JSONB, jwtSecret: encryptedFields.vault('jwtSecret'), + lastActiveAt: DataTypes.DATE, + lastActiveIp: DataTypes.STRING, + lastSignedInAt: DataTypes.DATE, + lastSignedInIp: DataTypes.STRING, suspendedAt: DataTypes.DATE, suspendedById: DataTypes.UUID, }, @@ -53,6 +58,24 @@ User.associate = models => { }; // 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() { return JWT.sign({ id: this.id }, this.jwtSecret); };