diff --git a/apps/server/src/modules/account/domain/account.ts b/apps/server/src/modules/account/domain/account.ts index 82153ffee4a..2b15037c12c 100644 --- a/apps/server/src/modules/account/domain/account.ts +++ b/apps/server/src/modules/account/domain/account.ts @@ -13,6 +13,7 @@ export interface AccountProps extends AuthorizableObject { password?: string; token?: string; credentialHash?: string; + lastLogin?: Date; lasttriedFailedLogin?: Date; expiresAt?: Date; activated?: boolean; @@ -73,6 +74,14 @@ export class Account extends DomainObject { return this.props.credentialHash; } + public get lastLogin(): Date | undefined { + return this.props.lastLogin; + } + + public set lastLogin(lastLogin: Date | undefined) { + this.props.lastLogin = lastLogin; + } + public get lasttriedFailedLogin(): Date | undefined { return this.props.lasttriedFailedLogin; } diff --git a/apps/server/src/modules/account/domain/entity/account.entity.ts b/apps/server/src/modules/account/domain/entity/account.entity.ts index 1c82e085054..736e8897431 100644 --- a/apps/server/src/modules/account/domain/entity/account.entity.ts +++ b/apps/server/src/modules/account/domain/entity/account.entity.ts @@ -26,6 +26,10 @@ export class AccountEntity extends BaseEntityWithTimestamps { @Property({ nullable: true }) systemId?: ObjectId; + @Property({ nullable: true }) + @Index() + lastLogin?: Date; + @Property({ nullable: true }) lasttriedFailedLogin?: Date; @@ -47,6 +51,7 @@ export class AccountEntity extends BaseEntityWithTimestamps { this.userId = props.userId; this.systemId = props.systemId; this.lasttriedFailedLogin = props.lasttriedFailedLogin; + this.lastLogin = props.lastLogin; this.expiresAt = props.expiresAt; this.activated = props.activated; this.deactivatedAt = props.deactivatedAt; diff --git a/apps/server/src/modules/account/domain/services/account-db.service.spec.ts b/apps/server/src/modules/account/domain/services/account-db.service.spec.ts index 5ea9543837a..4b5911267a8 100644 --- a/apps/server/src/modules/account/domain/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account-db.service.spec.ts @@ -714,27 +714,40 @@ describe('AccountDbService', () => { }); }); + describe('updateLastLogin', () => { + const setup = () => { + const mockTeacherAccount = accountDoFactory.build(); + const theNewDate = new Date(); + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + + return { mockTeacherAccount, theNewDate }; + }; + + it('should update last tried failed login', async () => { + const { mockTeacherAccount, theNewDate } = setup(); + + const ret = await accountService.updateLastLogin(mockTeacherAccount.id, theNewDate); + + expect(ret.lastLogin).toEqual(theNewDate); + }); + }); + describe('updateLastTriedFailedLogin', () => { - describe('when update last failed Login', () => { - const setup = () => { - const mockTeacherAccount = accountDoFactory.build(); - const theNewDate = new Date(); + const setup = () => { + const mockTeacherAccount = accountDoFactory.build(); + const theNewDate = new Date(); - accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountRepo.findById.mockResolvedValue(mockTeacherAccount); - return { mockTeacherAccount, theNewDate }; - }; + return { mockTeacherAccount, theNewDate }; + }; - it('should update last tried failed login', async () => { - const { mockTeacherAccount, theNewDate } = setup(); - const ret = await accountService.updateLastTriedFailedLogin(mockTeacherAccount.id, theNewDate); + it('should update last tried failed login', async () => { + const { mockTeacherAccount, theNewDate } = setup(); + const ret = await accountService.updateLastTriedFailedLogin(mockTeacherAccount.id, theNewDate); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - ...mockTeacherAccount.getProps(), - lasttriedFailedLogin: theNewDate, - }); - }); + expect(ret.lasttriedFailedLogin).toEqual(theNewDate); }); }); diff --git a/apps/server/src/modules/account/domain/services/account-db.service.ts b/apps/server/src/modules/account/domain/services/account-db.service.ts index b95ddafb1f0..36df25fb82b 100644 --- a/apps/server/src/modules/account/domain/services/account-db.service.ts +++ b/apps/server/src/modules/account/domain/services/account-db.service.ts @@ -64,6 +64,14 @@ export class AccountServiceDb extends AbstractAccountService { return account; } + async updateLastLogin(accountId: EntityId, lastLogin: Date): Promise { + const internalId = await this.getInternalId(accountId); + const account = await this.accountRepo.findById(internalId); + account.lastLogin = lastLogin; + await this.accountRepo.save(account); + return account; + } + async updateLastTriedFailedLogin(accountId: EntityId, lastTriedFailedLogin: Date): Promise { const internalId = await this.getInternalId(accountId); const account = await this.accountRepo.findById(internalId); diff --git a/apps/server/src/modules/account/domain/services/account.service.spec.ts b/apps/server/src/modules/account/domain/services/account.service.spec.ts index cd2cbeea0e7..77d9d7ebf35 100644 --- a/apps/server/src/modules/account/domain/services/account.service.spec.ts +++ b/apps/server/src/modules/account/domain/services/account.service.spec.ts @@ -579,6 +579,16 @@ describe('AccountService', () => { }); }); + describe('updateLastLogin', () => { + it('should call updateLastLogin in accountServiceDb', async () => { + const someId = new ObjectId().toHexString(); + + await accountService.updateLastLogin(someId, new Date()); + + expect(accountServiceDb.updateLastLogin).toHaveBeenCalledTimes(1); + }); + }); + describe('updateLastTriedFailedLogin', () => { describe('When calling updateLastTriedFailedLogin in accountService', () => { it('should call updateLastTriedFailedLogin in accountServiceDb', async () => { diff --git a/apps/server/src/modules/account/domain/services/account.service.ts b/apps/server/src/modules/account/domain/services/account.service.ts index 042ab5a0f78..877332b5a53 100644 --- a/apps/server/src/modules/account/domain/services/account.service.ts +++ b/apps/server/src/modules/account/domain/services/account.service.ts @@ -348,6 +348,10 @@ export class AccountService extends AbstractAccountService implements DeletionSe return new Account({ ...ret.getProps(), idmReferenceId: idmAccount?.idmReferenceId }); } + async updateLastLogin(accountId: string, lastLogin: Date): Promise { + await this.accountDb.updateLastLogin(accountId, lastLogin); + } + async updateLastTriedFailedLogin(accountId: string, lastTriedFailedLogin: Date): Promise { const ret = await this.accountDb.updateLastTriedFailedLogin(accountId, lastTriedFailedLogin); const idmAccount = await this.executeIdmMethod(async () => { diff --git a/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts b/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts index 173e7e98d72..bf2bd4f5b47 100644 --- a/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts +++ b/apps/server/src/modules/account/repo/micro-orm/mapper/account-do-to-entity.mapper.ts @@ -10,6 +10,7 @@ export class AccountDoToEntityMapper { activated: account.activated, credentialHash: account.credentialHash, expiresAt: account.expiresAt, + lastLogin: account.lastLogin, lasttriedFailedLogin: account.lasttriedFailedLogin, password: account.password, systemId: account.systemId ? new ObjectId(account.systemId) : undefined, diff --git a/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts b/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts index 59b514840cf..000ece5001a 100644 --- a/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts +++ b/apps/server/src/modules/account/repo/micro-orm/mapper/account-entity-to-do.mapper.ts @@ -13,6 +13,7 @@ export class AccountEntityToDoMapper { activated: account.activated, credentialHash: account.credentialHash, expiresAt: account.expiresAt, + lastLogin: account.lastLogin, lasttriedFailedLogin: account.lasttriedFailedLogin, password: account.password, systemId: account.systemId?.toString(), diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 51f10a7109a..32d6850f243 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -179,6 +179,14 @@ describe('AuthenticationService', () => { }); }); + describe('updateLastLogin', () => { + it('should call accountService to update last login', async () => { + await authenticationService.updateLastLogin('mockAccountId'); + + expect(accountService.updateLastLogin).toHaveBeenCalledWith('mockAccountId', expect.any(Date)); + }); + }); + describe('normalizeUsername', () => { describe('when a username is entered', () => { it('should trim username', () => { diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index 4a2b816b1e9..124a4c419b8 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -79,6 +79,10 @@ export class AuthenticationService { } } + async updateLastLogin(accountId: string): Promise { + await this.accountService.updateLastLogin(accountId, new Date()); + } + async updateLastTriedFailedLogin(id: string): Promise { await this.accountService.updateLastTriedFailedLogin(id, new Date()); } diff --git a/apps/server/src/modules/authentication/uc/login.uc.spec.ts b/apps/server/src/modules/authentication/uc/login.uc.spec.ts index c0f1d924876..4b0d356402a 100644 --- a/apps/server/src/modules/authentication/uc/login.uc.spec.ts +++ b/apps/server/src/modules/authentication/uc/login.uc.spec.ts @@ -63,6 +63,14 @@ describe('LoginUc', () => { }); }); + it('should call updateLastLogin', async () => { + const { userInfo } = setup(); + + await loginUc.getLoginData(userInfo); + + expect(authenticationService.updateLastLogin).toHaveBeenCalledWith(userInfo.accountId); + }); + it('should return a loginDto', async () => { const { userInfo, loginDto } = setup(); diff --git a/apps/server/src/modules/authentication/uc/login.uc.ts b/apps/server/src/modules/authentication/uc/login.uc.ts index 80ab89ca49a..a676e0d79d3 100644 --- a/apps/server/src/modules/authentication/uc/login.uc.ts +++ b/apps/server/src/modules/authentication/uc/login.uc.ts @@ -14,6 +14,8 @@ export class LoginUc { const accessTokenDto: LoginDto = await this.authService.generateJwt(createJwtPayload); + await this.authService.updateLastLogin(userInfo.accountId); + const loginDto: LoginDto = new LoginDto({ accessToken: accessTokenDto.accessToken, }); diff --git a/src/services/authentication/strategies/TSPStrategy.js b/src/services/authentication/strategies/TSPStrategy.js index e615aa56857..d856578df0f 100644 --- a/src/services/authentication/strategies/TSPStrategy.js +++ b/src/services/authentication/strategies/TSPStrategy.js @@ -177,6 +177,10 @@ class TSPStrategy extends AuthenticationBaseStrategy { // find account and generate JWT payload const account = await app.service('nest-account-service').findByUserId(user._id.toString()); account._id = account.id; + + const now = new Date(); + await app.service('nest-account-service').updateLastLogin(account.id, now); + const { entity } = this.configuration; return { authentication: { strategy: this.name },