diff --git a/src/db/models/User.ts b/src/db/models/User.ts index 309388a..c9c6eed 100644 --- a/src/db/models/User.ts +++ b/src/db/models/User.ts @@ -22,6 +22,9 @@ export class User { @Property({ type: 'date' }) createdAt: Date = new Date(); + @Property({ type: 'date', nullable: true }) + lastLoggedIn?: Date; + @Property({ name: 'permissions', type: types.array }) roles: string[] = []; diff --git a/src/modules/jobs/list/sessionExpiry.ts b/src/modules/jobs/list/sessionExpiry.ts index 5ab5de1..51740d5 100644 --- a/src/modules/jobs/list/sessionExpiry.ts +++ b/src/modules/jobs/list/sessionExpiry.ts @@ -1,9 +1,9 @@ import { Session } from '@/db/models/Session'; +import { User } from '@/db/models/User'; import { job } from '@/modules/jobs/job'; - // every day at 12:00:00 -export const sessionExpiryJob = job('0 12 * * *', async ({ em }) => { - await em +export const sessionExpiryJob = job('0 12 * * *', async ({ em, log }) => { + const deletedSessions = await em .createQueryBuilder(Session) .delete() .where({ @@ -11,5 +11,39 @@ export const sessionExpiryJob = job('0 12 * * *', async ({ em }) => { $lt: new Date(), }, }) - .execute(); + .execute<{ affectedRows: number }>('run'); + + log.info(`Removed ${deletedSessions.affectedRows} sessions that had expired`); + + const knex = em.getKnex(); + + // Count all sessions for a user ID + const sessionCountForUser = em + .createQueryBuilder(Session, 'session') + .count() + .where({ user: knex.ref('user.id') }) + .getKnexQuery(); + + const now = new Date(); + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(now.getFullYear() - 1); + + // Delete all users who do not have any sessions AND + // (their login date is null OR they last logged in over 1 year ago) + const deletedUsers = await em + .createQueryBuilder(User, 'user') + .delete() + .withSubQuery(sessionCountForUser, 'session.sessionCount') + .where({ + 'session.sessionCount': 0, + $or: [ + { lastLoggedIn: { $eq: undefined } }, + { lastLoggedIn: { $lt: oneYearAgo } }, + ], + }) + .execute<{ affectedRows: number }>('run'); + + log.info( + `Removed ${deletedUsers.affectedRows} users older than 1 year with no sessions`, + ); }); diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts index fb11f50..0b9798d 100644 --- a/src/routes/auth/login.ts +++ b/src/routes/auth/login.ts @@ -62,13 +62,15 @@ export const loginAuthRouter = makeRouter((app) => { throw new StatusError('User cannot be found', 401); } + user.lastLoggedIn = new Date(); + const session = makeSession( user.id, body.device, req.headers['user-agent'], ); - await em.persistAndFlush(session); + await em.persistAndFlush([session, user]); return { session: formatSession(session), diff --git a/src/routes/auth/manage.ts b/src/routes/auth/manage.ts index 0a7b67b..22927ac 100644 --- a/src/routes/auth/manage.ts +++ b/src/routes/auth/manage.ts @@ -64,11 +64,14 @@ export const manageAuthRouter = makeRouter((app) => { user.namespace = body.namespace; user.publicKey = body.publicKey; user.profile = body.profile; + user.lastLoggedIn = new Date(); + const session = makeSession( user.id, body.device, req.headers['user-agent'], ); + await em.persistAndFlush([user, session]); getMetrics().user.inc({ namespace: body.namespace }, 1); return {