Merge pull request #4717 from freesewing/joost
feat(backend): Track lastSeen and jwtCalls/keyCalls in middleware
This commit is contained in:
commit
7ed8175bb8
3 changed files with 69 additions and 8 deletions
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue