diff --git a/src/lib/db/user-store.ts b/src/lib/db/user-store.ts index 80272b7aec..dc9eacc55c 100644 --- a/src/lib/db/user-store.ts +++ b/src/lib/db/user-store.ts @@ -22,6 +22,7 @@ const USER_COLUMNS_PUBLIC = [ 'email', 'image_url', 'seen_at', + 'first_seen_at', 'is_service', 'scim_id', ]; @@ -58,6 +59,7 @@ const rowToUser = (row) => { imageUrl: emptify(row.image_url), loginAttempts: row.login_attempts, seenAt: row.seen_at, + firstSeenAt: row.first_seen_at, createdAt: row.created_at, isService: row.is_service, scimId: row.scim_id, @@ -233,6 +235,9 @@ class UserStore implements IUserStore { return this.buildSelectUser(user).update({ login_attempts: 0, seen_at: new Date(), + first_seen_at: this.db.raw('COALESCE(first_seen_at, ?)', [ + new Date(), + ]), }); } diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts index 5569daf856..c40c87609a 100644 --- a/src/lib/types/user.ts +++ b/src/lib/types/user.ts @@ -11,6 +11,7 @@ export interface UserData { email?: string; imageUrl?: string; seenAt?: Date; + firstSeenAt?: Date; loginAttempts?: number; createdAt?: Date; isService?: boolean; @@ -24,6 +25,7 @@ export interface IUser { email?: string; inviteLink?: string; seenAt?: Date; + firstSeenAt?: Date; createdAt?: Date; permissions: string[]; loginAttempts?: number; @@ -75,6 +77,7 @@ export default class User implements IUser { username, imageUrl, seenAt, + firstSeenAt, loginAttempts, createdAt, isService, @@ -93,6 +96,7 @@ export default class User implements IUser { this.email = email!; this.imageUrl = imageUrl || this.generateImageUrl(); this.seenAt = seenAt; + this.firstSeenAt = firstSeenAt; this.loginAttempts = loginAttempts; this.createdAt = createdAt; this.accountType = isService ? 'Service Account' : 'User'; diff --git a/src/test/e2e/stores/user-store.e2e.test.ts b/src/test/e2e/stores/user-store.e2e.test.ts index 5ba6dae575..d7bbc30d1f 100644 --- a/src/test/e2e/stores/user-store.e2e.test.ts +++ b/src/test/e2e/stores/user-store.e2e.test.ts @@ -108,6 +108,22 @@ test('should reset user after successful login', async () => { expect(storedUser.seenAt! >= user.seenAt!).toBe(true); }); +test('should track and keep first login', async () => { + const store = stores.userStore; + const user = await store.insert({ email: 'firstlogin@mail.com' }); + await store.successfullyLogin(user); + + const storedUser = await store.getByQuery(user); + + expect(storedUser.seenAt).toEqual(storedUser.firstSeenAt); + + await store.successfullyLogin(user); + + const newLoginUser = await store.getByQuery(user); + + expect(storedUser.seenAt).toEqual(newLoginUser.firstSeenAt); +}); + test('should only update specified fields on user', async () => { const store = stores.userStore; const email = 'usertobeupdated@mail.com';