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?
|
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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue