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?
initial String
imperial Boolean @default(false)
jwtCalls Int @default(0)
keyCalls Int @default(0)
language String @default("en")
lastSignIn DateTime?
lastSeen DateTime?
lusername String @unique
mfaSecret String @default("")
mfaEnabled Boolean @default(false)

View file

@ -2,6 +2,18 @@ import cors from 'cors'
import http from 'passport-http'
import jwt from 'passport-jwt'
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) {
app.use(cors())
@ -12,6 +24,11 @@ function loadPassportMiddleware(passport, tools) {
new http.BasicStrategy(async (key, secret, done) => {
const Apikey = new ApikeyModel(tools)
await Apikey.verify(key, secret)
/*
* Update lastSeen field
*/
if (Apikey.verified) await updateLastSeen(Apikey.record.userId, tools, 'key')
return Apikey.verified
? done(null, { ...Apikey.record, apikey: true, uid: Apikey.record.userId })
: done(false)
@ -23,7 +40,12 @@ function loadPassportMiddleware(passport, tools) {
jwtFromRequest: jwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
...tools.config.jwt,
},
(jwt_payload, done) => {
async (jwt_payload, done) => {
/*
* Update lastSeen field
*/
await updateLastSeen(jwt_payload._id, tools, 'jwt')
return done(null, {
...jwt_payload,
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
* if this happens to be a v2 user.
*/
const update = { lastSignIn: new Date() }
if (updatedPasswordField) update.password = updatedPasswordField
await this.update(update)
if (updatedPasswordField) await this.update({ password: updatedPasswordField })
/*
* Final check for account status and other things before returning
@ -703,7 +701,6 @@ UserModel.prototype.confirm = async function ({ body, params }) {
await this.update({
status: 1,
consent: body.consent,
lastSignIn: new Date(),
})
/*
@ -1099,8 +1096,10 @@ UserModel.prototype.asAccount = function () {
img: this.clear.img,
imperial: this.record.imperial,
initial: this.clear.initial,
jwtCalls: this.record.jwtCalls,
keyCalls: this.record.keyCalls,
language: this.record.language,
lastSignIn: this.record.lastSignIn,
lastSeen: this.record.lastSeen,
mfaEnabled: this.record.mfaEnabled,
newsletter: this.record.newsletter,
patron: this.record.patron,
@ -1109,6 +1108,10 @@ UserModel.prototype.asAccount = function () {
updatedAt: this.record.updatedAt,
username: this.record.username,
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
}
/*
* 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.
* This can all be removed after v3 is in production and all users have been migrated.
@ -1228,7 +1265,7 @@ const migrateUser = (v2) => {
initial,
imperial: v2.units === 'imperial',
language: v2.settings.language,
lastSignIn: v2.time?.login ? v2.time.login : null,
lastSeen: Date.now(),
lusername: v2.username.toLowerCase(),
mfaEnabled: false,
newsletter: false,