diff --git a/src/lib/db/user-store.ts b/src/lib/db/user-store.ts index ae3182f417..7794742215 100644 --- a/src/lib/db/user-store.ts +++ b/src/lib/db/user-store.ts @@ -228,6 +228,17 @@ class UserStore implements IUserStore { .first(); return rowToUser(row); } + + async markSeenAt(secrets: string[]): Promise { + const now = new Date(); + try { + await this.db('personal_access_tokens') + .whereIn('secret', secrets) + .update({ seen_at: now }); + } catch (err) { + this.logger.error('Could not update lastSeen, error: ', err); + } + } } module.exports = UserStore; diff --git a/src/lib/middleware/pat-middleware.ts b/src/lib/middleware/pat-middleware.ts index c2009fabbb..933d159f3a 100644 --- a/src/lib/middleware/pat-middleware.ts +++ b/src/lib/middleware/pat-middleware.ts @@ -17,6 +17,7 @@ const patMiddleware = ( apiToken, ); req.user = user; + userService.addPATSeen(apiToken); } } catch (error) { logger.error(error); diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index a10cd0b892..319c3c6888 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -28,6 +28,8 @@ import PasswordMismatch from '../error/password-mismatch'; import BadDataError from '../error/bad-data-error'; import { isDefined } from '../util/isDefined'; import { TokenUserSchema } from '../openapi/spec/token-user-schema'; +import { IFlagResolver } from 'lib/types/experimental'; +import { minutesToMilliseconds } from 'date-fns'; const systemUser = new User({ id: -1, username: 'system' }); @@ -78,12 +80,22 @@ class UserService { private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {}; + private seenTimer: NodeJS.Timeout; + + private lastSeenSecrets: Set = new Set(); + + private flagResolver: IFlagResolver; + constructor( stores: Pick, { getLogger, authentication, - }: Pick, + flagResolver, + }: Pick< + IUnleashConfig, + 'getLogger' | 'authentication' | 'flagResolver' + >, services: { accessService: AccessService; resetTokenService: ResetTokenService; @@ -92,6 +104,7 @@ class UserService { settingService: SettingService; }, ) { + this.flagResolver = flagResolver; this.logger = getLogger('service/user-service.js'); this.store = stores.userStore; this.eventStore = stores.eventStore; @@ -103,6 +116,9 @@ class UserService { if (authentication && authentication.createAdminUser) { process.nextTick(() => this.initAdminUser()); } + if (this.flagResolver.isEnabled('tokensLastSeen')) { + this.updateLastSeen(); + } } validatePassword(password: string): boolean { @@ -426,6 +442,30 @@ class UserService { async getUserByPersonalAccessToken(secret: string): Promise { return this.store.getUserByPersonalAccessToken(secret); } + + async updateLastSeen(): Promise { + if (this.lastSeenSecrets.size > 0) { + const toStore = [...this.lastSeenSecrets]; + this.lastSeenSecrets = new Set(); + await this.store.markSeenAt(toStore); + } + + this.seenTimer = setTimeout( + async () => this.updateLastSeen(), + minutesToMilliseconds(3), + ).unref(); + } + + addPATSeen(secret: string): void { + if (this.flagResolver.isEnabled('tokensLastSeen')) { + this.lastSeenSecrets.add(secret); + } + } + + destroy(): void { + clearTimeout(this.seenTimer); + this.seenTimer = null; + } } module.exports = UserService; diff --git a/src/lib/types/stores/user-store.ts b/src/lib/types/stores/user-store.ts index 6441a8d147..b014f5f7a1 100644 --- a/src/lib/types/stores/user-store.ts +++ b/src/lib/types/stores/user-store.ts @@ -33,4 +33,5 @@ export interface IUserStore extends Store { successfullyLogin(user: IUser): Promise; count(): Promise; getUserByPersonalAccessToken(secret: string): Promise; + markSeenAt(secrets: string[]): Promise; } diff --git a/src/test/fixtures/fake-user-store.ts b/src/test/fixtures/fake-user-store.ts index b6f3fc5de2..009a3bdb08 100644 --- a/src/test/fixtures/fake-user-store.ts +++ b/src/test/fixtures/fake-user-store.ts @@ -142,6 +142,11 @@ class UserStoreMock implements IUserStore { getUserByPersonalAccessToken(secret: string): Promise { return Promise.resolve(undefined); } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async markSeenAt(secrets: string[]): Promise { + throw new Error('Not implemented'); + } } module.exports = UserStoreMock;