1
0
Fork 0

Merge pull request #4717 from freesewing/joost

feat(backend): Track lastSeen and jwtCalls/keyCalls in middleware
This commit is contained in:
Joost De Cock 2023-08-13 10:55:21 +02:00 committed by GitHub
commit 7ed8175bb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 69 additions and 8 deletions

View file

@ -54,8 +54,10 @@ model User {
img String? img String?
initial String initial String
imperial Boolean @default(false) imperial Boolean @default(false)
jwtCalls Int @default(0)
keyCalls Int @default(0)
language String @default("en") language String @default("en")
lastSignIn DateTime? lastSeen DateTime?
lusername String @unique lusername String @unique
mfaSecret String @default("") mfaSecret String @default("")
mfaEnabled Boolean @default(false) mfaEnabled Boolean @default(false)

View file

@ -2,6 +2,18 @@ import cors from 'cors'
import http from 'passport-http' import http from 'passport-http'
import jwt from 'passport-jwt' import jwt from 'passport-jwt'
import { ApikeyModel } from './models/apikey.mjs' import { ApikeyModel } from './models/apikey.mjs'
import { UserModel } from './models/user.mjs'
/*
* In v2 we ended up with a bug where we did not properly track the last login
* So in v3 we switch to `lastSeen` and every authenticated API call we update
* this field. It's a bit of a perf hit to write to the database on ever API call
* but it's worth it to actually know which accounts are used and which are not.
*/
async function updateLastSeen(uid, tools, type) {
const User = new UserModel(tools)
await User.seen(uid, type)
}
function loadExpressMiddleware(app) { function loadExpressMiddleware(app) {
app.use(cors()) app.use(cors())
@ -12,6 +24,11 @@ function loadPassportMiddleware(passport, tools) {
new http.BasicStrategy(async (key, secret, done) => { new http.BasicStrategy(async (key, secret, done) => {
const Apikey = new ApikeyModel(tools) const Apikey = new ApikeyModel(tools)
await Apikey.verify(key, secret) await Apikey.verify(key, secret)
/*
* Update lastSeen field
*/
if (Apikey.verified) await updateLastSeen(Apikey.record.userId, tools, 'key')
return Apikey.verified return Apikey.verified
? done(null, { ...Apikey.record, apikey: true, uid: Apikey.record.userId }) ? done(null, { ...Apikey.record, apikey: true, uid: Apikey.record.userId })
: done(false) : done(false)
@ -23,7 +40,12 @@ function loadPassportMiddleware(passport, tools) {
jwtFromRequest: jwt.ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: jwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
...tools.config.jwt, ...tools.config.jwt,
}, },
(jwt_payload, done) => { async (jwt_payload, done) => {
/*
* Update lastSeen field
*/
await updateLastSeen(jwt_payload._id, tools, 'jwt')
return done(null, { return done(null, {
...jwt_payload, ...jwt_payload,
uid: jwt_payload._id, uid: jwt_payload._id,

View file

@ -461,9 +461,7 @@ UserModel.prototype.passwordSignIn = async function (req) {
* have their password and we know it's good, let's rehash it the v3 way * have their password and we know it's good, let's rehash it the v3 way
* if this happens to be a v2 user. * if this happens to be a v2 user.
*/ */
const update = { lastSignIn: new Date() } if (updatedPasswordField) await this.update({ password: updatedPasswordField })
if (updatedPasswordField) update.password = updatedPasswordField
await this.update(update)
/* /*
* Final check for account status and other things before returning * Final check for account status and other things before returning
@ -703,7 +701,6 @@ UserModel.prototype.confirm = async function ({ body, params }) {
await this.update({ await this.update({
status: 1, status: 1,
consent: body.consent, consent: body.consent,
lastSignIn: new Date(),
}) })
/* /*
@ -1099,8 +1096,10 @@ UserModel.prototype.asAccount = function () {
img: this.clear.img, img: this.clear.img,
imperial: this.record.imperial, imperial: this.record.imperial,
initial: this.clear.initial, initial: this.clear.initial,
jwtCalls: this.record.jwtCalls,
keyCalls: this.record.keyCalls,
language: this.record.language, language: this.record.language,
lastSignIn: this.record.lastSignIn, lastSeen: this.record.lastSeen,
mfaEnabled: this.record.mfaEnabled, mfaEnabled: this.record.mfaEnabled,
newsletter: this.record.newsletter, newsletter: this.record.newsletter,
patron: this.record.patron, patron: this.record.patron,
@ -1109,6 +1108,10 @@ UserModel.prototype.asAccount = function () {
updatedAt: this.record.updatedAt, updatedAt: this.record.updatedAt,
username: this.record.username, username: this.record.username,
lusername: this.record.lusername, lusername: this.record.lusername,
/*
* Add this so we can give a note to users about migrating their password
*/
passwordType: JSON.parse(this.record.password).type,
} }
} }
@ -1206,6 +1209,40 @@ UserModel.prototype.isLusernameAvailable = async function (lusername) {
return true return true
} }
/*
* Helper method to update the `lastSeen` field of the user
* This is called from middleware with the user ID passed in.
*
* @param {id} string - The user ID
* @param {type} string - The authentication type (one of 'jwt' or 'key')
* @returns {success} boolean - True if it worked, false if not
*/
UserModel.prototype.seen = async function (id, type) {
/*
* Construct data object for update operation
*/
const data = { lastSeen: new Date() }
data[`${type}Calls`] = { increment: 1 }
/*
* Now update the dabatase record
*/
try {
await this.prisma.user.update({ where: { id }, data })
} catch (err) {
/*
* An error means it's not good. Return false
*/
log.warn({ id, err }, 'Could not update lastSeen field from middleware')
return false
}
/*
* If we get here, the lastSeen field was updated and user exists, so return true
*/
return true
}
/* /*
* Everything below this comment is migration code. * Everything below this comment is migration code.
* This can all be removed after v3 is in production and all users have been migrated. * This can all be removed after v3 is in production and all users have been migrated.
@ -1228,7 +1265,7 @@ const migrateUser = (v2) => {
initial, initial,
imperial: v2.units === 'imperial', imperial: v2.units === 'imperial',
language: v2.settings.language, language: v2.settings.language,
lastSignIn: v2.time?.login ? v2.time.login : null, lastSeen: Date.now(),
lusername: v2.username.toLowerCase(), lusername: v2.username.toLowerCase(),
mfaEnabled: false, mfaEnabled: false,
newsletter: false, newsletter: false,