Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-3836 Save last login of account in database #5077

Merged
merged 15 commits into from
Jul 1, 2024
9 changes: 9 additions & 0 deletions apps/server/src/modules/account/domain/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface AccountProps extends AuthorizableObject {
password?: string;
token?: string;
credentialHash?: string;
lastLogin?: Date;
lasttriedFailedLogin?: Date;
expiresAt?: Date;
activated?: boolean;
Expand Down Expand Up @@ -73,6 +74,14 @@ export class Account extends DomainObject<AccountProps> {
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export class AccountServiceDb extends AbstractAccountService {
return account;
}

async updateLastLogin(accountId: EntityId, lastLogin: Date): Promise<Account> {
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<Account> {
const internalId = await this.getInternalId(accountId);
const account = await this.accountRepo.findById(internalId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,14 @@ describe('AccountService', () => {
});
});

describe('updateFailedLogin', () => {
it('should call updateLastLogin in accountServiceDb', async () => {
await accountService.updateLastLogin('accountId', new Date());
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved

expect(accountServiceDb.updateLastLogin).toHaveBeenCalledTimes(1);
});
});

describe('updateLastTriedFailedLogin', () => {
describe('When calling updateLastTriedFailedLogin in accountService', () => {
it('should call updateLastTriedFailedLogin in accountServiceDb', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved
await this.accountDb.updateLastLogin(accountId, lastLogin);
}

async updateLastTriedFailedLogin(accountId: string, lastTriedFailedLogin: Date): Promise<Account> {
const ret = await this.accountDb.updateLastTriedFailedLogin(accountId, lastTriedFailedLogin);
const idmAccount = await this.executeIdmMethod(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { AccountModule } from '../account';
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved
import { AuthenticationModule } from './authentication.module';
import { LoginController } from './controllers/login.controller';
import { LoginUc } from './uc/login.uc';

@Module({
imports: [AuthenticationModule],
imports: [AuthenticationModule, AccountModule],
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved
providers: [LoginUc],
controllers: [LoginController],
exports: [],
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/modules/authentication/uc/login.uc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { AccountService } from '@src/modules/account';
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved
import { AuthenticationService } from '../services/authentication.service';
import { LoginDto } from './dto';
import { LoginUc } from './login.uc';
Expand All @@ -9,6 +10,7 @@ describe('LoginUc', () => {
let loginUc: LoginUc;

let authenticationService: DeepMocked<AuthenticationService>;
let accountService: DeepMocked<AccountService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand All @@ -18,11 +20,16 @@ describe('LoginUc', () => {
provide: AuthenticationService,
useValue: createMock<AuthenticationService>(),
},
{
provide: AccountService,
useValue: createMock<AccountService>(),
},
],
}).compile();

loginUc = await module.get(LoginUc);
authenticationService = await module.get(AuthenticationService);
accountService = await module.get(AccountService);
});

describe('getLoginData', () => {
Expand Down Expand Up @@ -63,6 +70,14 @@ describe('LoginUc', () => {
});
});

it('should call updateLastLogin', async () => {
const { userInfo } = setup();

await loginUc.getLoginData(userInfo);

expect(accountService.updateLastLogin).toHaveBeenCalledWith(userInfo.accountId, expect.any(Date));
});

it('should return a loginDto', async () => {
const { userInfo, loginDto } = setup();

Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/modules/authentication/uc/login.uc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AccountService } from '@src/modules/account';
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved
import { ICurrentUser } from '../interface';
import { CreateJwtPayload } from '../interface/jwt-payload';
import { CurrentUserMapper } from '../mapper';
Expand All @@ -7,13 +8,16 @@ import { LoginDto } from './dto';

@Injectable()
export class LoginUc {
constructor(private readonly authService: AuthenticationService) {}
constructor(private readonly authService: AuthenticationService, private readonly accountService: AccountService) {}

async getLoginData(userInfo: ICurrentUser): Promise<LoginDto> {
const createJwtPayload: CreateJwtPayload = CurrentUserMapper.mapCurrentUserToCreateJwtPayload(userInfo);

const accessTokenDto: LoginDto = await this.authService.generateJwt(createJwtPayload);

const now = new Date();
await this.accountService.updateLastLogin(userInfo.accountId, now);

const loginDto: LoginDto = new LoginDto({
accessToken: accessTokenDto.accessToken,
});
Expand Down
4 changes: 4 additions & 0 deletions src/services/authentication/strategies/TSPStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Loading