From f850adf2618a63d3bf773a1e3d8eb7ed62ccee37 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Thu, 16 Jan 2025 18:31:33 -0300 Subject: [PATCH 1/7] chore: add auth history feature --- packages/nestjs-auth-apple/package.json | 1 + .../src/auth-apple.constants.ts | 2 + .../src/auth-apple.controller.ts | 39 +++++- .../src/auth-apple.module.spec.ts | 2 + .../events/auth-apple-authenticated.event.ts | 7 + packages/nestjs-auth-history/README.md | 15 ++ packages/nestjs-auth-history/package.json | 46 +++++++ .../src/__fixtures__/app.module.fixture.ts | 46 +++++++ .../create-auth-history-repository.fixture.ts | 73 ++++++++++ .../entities/auth-history.entity.fixture.ts | 10 ++ .../entities/user-entity.fixture.ts | 12 ++ .../events/invitation-accepted.event.ts | 7 + .../events/invitation-get-user.event.ts | 10 ++ .../src/__fixtures__/ormconfig.fixture.ts | 10 ++ .../user-lookup.custom.service.ts | 30 ++++ .../src/auth-history.constants.ts | 11 ++ .../src/auth-history.controller.e2e-spec.ts | 123 +++++++++++++++++ .../src/auth-history.controller.ts | 107 ++++++++++++++ .../src/auth-history.factory.ts | 36 +++++ .../src/auth-history.module-definition.ts | 130 ++++++++++++++++++ .../src/auth-history.module.spec.ts | 48 +++++++ .../src/auth-history.module.ts | 29 ++++ .../src/auth-history.seeder.ts | 34 +++++ .../src/auth-history.types.spec.ts | 10 ++ .../src/auth-history.types.ts | 4 + .../src/config/auth-history-default.config.ts | 11 ++ .../src/dto/auth-history-create.dto.ts | 20 +++ .../src/dto/auth-history-paginated.dto.ts | 20 +++ .../src/dto/auth-history.dto.ts | 57 ++++++++ .../entities/auth-history-postgres.entity.ts | 41 ++++++ .../entities/auth-history-sqlite.entity.ts | 38 +++++ .../auth-history-bad-request-exception.ts | 14 ++ .../src/exceptions/auth-history-exception.ts | 13 ++ .../auth-history-not-found-exception.ts | 15 ++ packages/nestjs-auth-history/src/index.ts | 21 +++ ...auth-history-entities-options.interface.ts | 9 ++ .../auth-history-entity.interface.ts | 8 ++ .../auth-history-mutate-service.interface.ts | 11 ++ .../auth-history-options-extras.interface.ts | 6 + .../auth-history-options.interface.ts | 7 + .../auth-history-settings.interface.ts | 11 ++ .../src/listeners/authenticated-listener.ts | 65 +++++++++ packages/nestjs-auth-history/src/seeding.ts | 7 + .../src/services/auth-history-crud.service.ts | 24 ++++ .../services/auth-history-mutate.service.ts | 33 +++++ .../services/auth-history-query.service.ts | 13 ++ packages/nestjs-auth-history/tsconfig.json | 20 +++ packages/nestjs-auth-history/typedoc.json | 3 + packages/nestjs-auth-local/package.json | 1 + .../src/__fixtures__/app.module.fixture.ts | 2 + .../src/auth-local.constants.ts | 2 + .../src/auth-local.controller.spec.ts | 30 +++- .../src/auth-local.controller.ts | 59 ++++++-- .../src/auth-local.module.spec.ts | 2 + .../events/auth-local-authenticated.event.ts | 7 + packages/nestjs-auth-local/src/index.ts | 1 + .../src/decorators/auth-info.decorator.ts | 23 ++++ .../auth-history-creatable.interface.ts | 4 + .../auth-history-ownable.interface.ts | 7 + .../interfaces/auth-history.interface.ts | 14 ++ .../authenticated-event-payload.interface.ts | 7 + .../authenticated-info.interface.ts | 8 ++ .../authenticated-user-info.interface.ts | 4 + packages/nestjs-common/src/domain/index.ts | 8 ++ packages/nestjs-common/src/index.ts | 1 + .../src/services/typeorm-crud.service.ts | 1 + .../src/03-authentication/app.module.ts | 2 + tsconfig.json | 3 + yarn.lock | 32 +++++ 69 files changed, 1511 insertions(+), 16 deletions(-) create mode 100644 packages/nestjs-auth-apple/src/events/auth-apple-authenticated.event.ts create mode 100644 packages/nestjs-auth-history/README.md create mode 100644 packages/nestjs-auth-history/package.json create mode 100644 packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/create-auth-history-repository.fixture.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/entities/auth-history.entity.fixture.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/entities/user-entity.fixture.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/events/invitation-accepted.event.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/events/invitation-get-user.event.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/ormconfig.fixture.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.constants.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.controller.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.factory.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.module-definition.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.module.spec.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.module.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.seeder.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.types.spec.ts create mode 100644 packages/nestjs-auth-history/src/auth-history.types.ts create mode 100644 packages/nestjs-auth-history/src/config/auth-history-default.config.ts create mode 100644 packages/nestjs-auth-history/src/dto/auth-history-create.dto.ts create mode 100644 packages/nestjs-auth-history/src/dto/auth-history-paginated.dto.ts create mode 100644 packages/nestjs-auth-history/src/dto/auth-history.dto.ts create mode 100644 packages/nestjs-auth-history/src/entities/auth-history-postgres.entity.ts create mode 100644 packages/nestjs-auth-history/src/entities/auth-history-sqlite.entity.ts create mode 100644 packages/nestjs-auth-history/src/exceptions/auth-history-bad-request-exception.ts create mode 100644 packages/nestjs-auth-history/src/exceptions/auth-history-exception.ts create mode 100644 packages/nestjs-auth-history/src/exceptions/auth-history-not-found-exception.ts create mode 100644 packages/nestjs-auth-history/src/index.ts create mode 100644 packages/nestjs-auth-history/src/interfaces/auth-history-entities-options.interface.ts create mode 100644 packages/nestjs-auth-history/src/interfaces/auth-history-entity.interface.ts create mode 100644 packages/nestjs-auth-history/src/interfaces/auth-history-mutate-service.interface.ts create mode 100644 packages/nestjs-auth-history/src/interfaces/auth-history-options-extras.interface.ts create mode 100644 packages/nestjs-auth-history/src/interfaces/auth-history-options.interface.ts create mode 100644 packages/nestjs-auth-history/src/interfaces/auth-history-settings.interface.ts create mode 100644 packages/nestjs-auth-history/src/listeners/authenticated-listener.ts create mode 100644 packages/nestjs-auth-history/src/seeding.ts create mode 100644 packages/nestjs-auth-history/src/services/auth-history-crud.service.ts create mode 100644 packages/nestjs-auth-history/src/services/auth-history-mutate.service.ts create mode 100644 packages/nestjs-auth-history/src/services/auth-history-query.service.ts create mode 100644 packages/nestjs-auth-history/tsconfig.json create mode 100644 packages/nestjs-auth-history/typedoc.json create mode 100644 packages/nestjs-auth-local/src/events/auth-local-authenticated.event.ts create mode 100644 packages/nestjs-common/src/decorators/auth-info.decorator.ts create mode 100644 packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-creatable.interface.ts create mode 100644 packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-ownable.interface.ts create mode 100644 packages/nestjs-common/src/domain/auth-history/interfaces/auth-history.interface.ts create mode 100644 packages/nestjs-common/src/domain/authentication/interfaces/authenticated-event-payload.interface.ts create mode 100644 packages/nestjs-common/src/domain/authentication/interfaces/authenticated-info.interface.ts create mode 100644 packages/nestjs-common/src/domain/authentication/interfaces/authenticated-user-info.interface.ts diff --git a/packages/nestjs-auth-apple/package.json b/packages/nestjs-auth-apple/package.json index e179d20e5..938e7c94c 100644 --- a/packages/nestjs-auth-apple/package.json +++ b/packages/nestjs-auth-apple/package.json @@ -15,6 +15,7 @@ "@concepta/nestjs-authentication": "^6.0.0-alpha.0", "@concepta/nestjs-common": "^6.0.0-alpha.0", "@concepta/nestjs-exception": "^6.0.0-alpha.0", + "@concepta/nestjs-event": "^6.0.0-alpha.0", "@concepta/nestjs-federated": "^6.0.0-alpha.0", "@concepta/nestjs-jwt": "^6.0.0-alpha.0", "@nestjs/common": "^10.4.1", diff --git a/packages/nestjs-auth-apple/src/auth-apple.constants.ts b/packages/nestjs-auth-apple/src/auth-apple.constants.ts index 6a7c5ef0d..0c60acb06 100644 --- a/packages/nestjs-auth-apple/src/auth-apple.constants.ts +++ b/packages/nestjs-auth-apple/src/auth-apple.constants.ts @@ -18,3 +18,5 @@ export const AUTH_APPLE_VERIFY_ALGORITHM = 'RS256'; export const AUTH_APPLE_TOKEN_ISSUER = 'https://appleid.apple.com'; export const AUTH_APPLE_JWT_KEYS = 'https://appleid.apple.com/auth/keys'; + +export const AUTH_APPLE_AUTHENTICATION_TYPE = 'auth-apple'; diff --git a/packages/nestjs-auth-apple/src/auth-apple.controller.ts b/packages/nestjs-auth-apple/src/auth-apple.controller.ts index 4afafd382..b4cbc2062 100644 --- a/packages/nestjs-auth-apple/src/auth-apple.controller.ts +++ b/packages/nestjs-auth-apple/src/auth-apple.controller.ts @@ -1,8 +1,11 @@ import { Controller, Inject, Get, UseGuards, Post } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { + AuthenticatedEventInterface, AuthenticatedUserInterface, AuthenticationResponseInterface, + AuthenticatedUserInfoInterface, + AuthInfo, } from '@concepta/nestjs-common'; import { AuthUser, @@ -10,8 +13,13 @@ import { AuthenticationJwtResponseDto, AuthPublic, } from '@concepta/nestjs-authentication'; -import { AUTH_APPLE_ISSUE_TOKEN_SERVICE_TOKEN } from './auth-apple.constants'; +import { + AUTH_APPLE_AUTHENTICATION_TYPE, + AUTH_APPLE_ISSUE_TOKEN_SERVICE_TOKEN, +} from './auth-apple.constants'; import { AuthAppleGuard } from './auth-apple.guard'; +import { AuthAppleAuthenticatedEventAsync } from './events/auth-apple-authenticated.event'; +import { EventDispatchService } from '@concepta/nestjs-event'; /** * Apple controller @@ -36,6 +44,7 @@ export class AuthAppleController { constructor( @Inject(AUTH_APPLE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, + private readonly eventDispatchService: EventDispatchService, ) {} /** @@ -57,7 +66,33 @@ export class AuthAppleController { @Post('callback') async post( @AuthUser() user: AuthenticatedUserInterface, + @AuthInfo() authInfo: AuthenticatedUserInfoInterface, ): Promise { - return this.issueTokenService.responsePayload(user.id); + const response = this.issueTokenService.responsePayload(user.id); + + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_APPLE_AUTHENTICATION_TYPE, + }, + }); + + return response; + } + + protected async dispatchAuthenticatedEvent( + payload?: AuthenticatedEventInterface, + ): Promise { + const authenticatedEventAsync = new AuthAppleAuthenticatedEventAsync( + payload, + ); + + const eventResult = await this.eventDispatchService.async( + authenticatedEventAsync, + ); + + return eventResult.every((it) => it === true); } } diff --git a/packages/nestjs-auth-apple/src/auth-apple.module.spec.ts b/packages/nestjs-auth-apple/src/auth-apple.module.spec.ts index 11e4046cd..2b3aa4e3f 100644 --- a/packages/nestjs-auth-apple/src/auth-apple.module.spec.ts +++ b/packages/nestjs-auth-apple/src/auth-apple.module.spec.ts @@ -16,6 +16,7 @@ import { AuthAppleModule } from './auth-apple.module'; import { FederatedEntityFixture } from './__fixtures__/federated-entity.fixture'; import { UserEntityFixture } from './__fixtures__/user.entity.fixture'; +import { EventModule } from '@concepta/nestjs-event'; describe(AuthAppleModule, () => { let authAppleModule: AuthAppleModule; @@ -31,6 +32,7 @@ describe(AuthAppleModule, () => { entities: [UserEntityFixture, FederatedEntityFixture], }), JwtModule.forRoot({}), + EventModule.forRoot({}), AuthAppleModule.forRoot({}), AuthenticationModule.forRoot({}), AuthJwtModule.forRootAsync({ diff --git a/packages/nestjs-auth-apple/src/events/auth-apple-authenticated.event.ts b/packages/nestjs-auth-apple/src/events/auth-apple-authenticated.event.ts new file mode 100644 index 000000000..df0e46311 --- /dev/null +++ b/packages/nestjs-auth-apple/src/events/auth-apple-authenticated.event.ts @@ -0,0 +1,7 @@ +import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; +import { EventAsync } from '@concepta/nestjs-event'; + +export class AuthAppleAuthenticatedEventAsync extends EventAsync< + AuthenticatedEventInterface, + boolean +> {} diff --git a/packages/nestjs-auth-history/README.md b/packages/nestjs-auth-history/README.md new file mode 100644 index 000000000..ffc6a7abc --- /dev/null +++ b/packages/nestjs-auth-history/README.md @@ -0,0 +1,15 @@ +# Rockets NestJS Auth History + +A module for tracking authentication history and events, providing services for creating, reading, updating and deleting auth history records. Includes event handling for authenticated requests, repository management, and access control. + +## Project + +[![NPM Latest](https://img.shields.io/npm/v/@concepta/nestjs-auth-history)](https://www.npmjs.com/package/@concepta/nestjs-auth-history) +[![NPM Downloads](https://img.shields.io/npm/dw/@conceptadev/nestjs-auth-history)](https://www.npmjs.com/package/@concepta/nestjs-auth-history) +[![GH Last Commit](https://img.shields.io/github/last-commit/conceptadev/rockets?logo=github)](https://github.com/conceptadev/rockets) +[![GH Contrib](https://img.shields.io/github/contributors/conceptadev/rockets?logo=github)](https://github.com/conceptadev/rockets/graphs/contributors) +[![NestJS Dep](https://img.shields.io/github/package-json/dependency-version/conceptadev/rockets/@nestjs/common?label=NestJS&logo=nestjs&filename=packages%2Fnestjs-core%2Fpackage.json)](https://www.npmjs.com/package/@nestjs/common) + +## Installation + +`yarn add @concepta/nestjs-auth-history` diff --git a/packages/nestjs-auth-history/package.json b/packages/nestjs-auth-history/package.json new file mode 100644 index 000000000..e4c72d5e6 --- /dev/null +++ b/packages/nestjs-auth-history/package.json @@ -0,0 +1,46 @@ +{ + "name": "@concepta/nestjs-auth-history", + "version": "6.0.0-alpha.0", + "description": "Rockets NestJS auth history", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/**/!(*.spec|*.e2e-spec|*.fixture).{js,d.ts}" + ], + "dependencies": { + "@concepta/nestjs-access-control": "^6.0.0-alpha.0", + "@concepta/nestjs-common": "^6.0.0-alpha.0", + "@concepta/nestjs-crud": "^6.0.0-alpha.0", + "@concepta/nestjs-event": "^6.0.0-alpha.0", + "@concepta/nestjs-exception": "^6.0.0-alpha.0", + "@concepta/nestjs-password": "^6.0.0-alpha.0", + "@concepta/nestjs-typeorm-ext": "^6.0.0-alpha.0", + "@concepta/typeorm-common": "^6.0.0-alpha.0", + "@nestjs/common": "^10.4.1", + "@nestjs/config": "^3.2.3", + "@nestjs/core": "^10.4.1", + "@nestjs/swagger": "^7.4.0" + }, + "devDependencies": { + "@concepta/nestjs-auth-jwt": "^6.0.0-alpha.0", + "@concepta/nestjs-authentication": "^6.0.0-alpha.0", + "@concepta/nestjs-jwt": "^6.0.0-alpha.0", + "@concepta/nestjs-user": "^6.0.0-alpha.0", + "@concepta/typeorm-seeding": "^4.0.0", + "@faker-js/faker": "^8.4.1", + "@nestjs/testing": "^10.4.1", + "@nestjs/typeorm": "^10.0.2", + "accesscontrol": "^2.2.1", + "supertest": "^6.3.4" + }, + "peerDependencies": { + "@concepta/nestjs-auth-local": "^6.0.0-alpha.0", + "class-transformer": "*", + "class-validator": "*", + "typeorm": "^0.3.0" + } +} diff --git a/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts new file mode 100644 index 000000000..0d8e713a0 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts @@ -0,0 +1,46 @@ +import { AccessControlModule } from '@concepta/nestjs-access-control'; +import { CrudModule } from '@concepta/nestjs-crud'; +import { EventModule } from '@concepta/nestjs-event'; +import { AuthLocalAuthenticatedEventAsync } from '@concepta/nestjs-auth-local'; +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; +import { Module } from '@nestjs/common'; +import { AccessControl } from 'accesscontrol'; + +import { AuthHistoryModule } from '../auth-history.module'; +import { AuthHistoryResource } from '../auth-history.types'; + +import { AuthHistoryAccessQueryService } from '../services/auth-history-query.service'; +import { AuthHistoryEntityFixture } from './entities/auth-history.entity.fixture'; +import { ormConfig } from './ormconfig.fixture'; + +const rules = new AccessControl(); +rules + .grant('auth-history') + .resource(AuthHistoryResource.One) + .createOwn() + .readOwn() + .updateOwn() + .deleteOwn(); + +@Module({ + imports: [ + TypeOrmExtModule.forRoot(ormConfig), + CrudModule.forRoot({}), + EventModule.forRoot({}), + AccessControlModule.forRoot({ + settings: { rules }, + queryServices: [AuthHistoryAccessQueryService], + }), + AuthHistoryModule.forRoot({ + settings: { + authenticatedEvents: [AuthLocalAuthenticatedEventAsync], + }, + entities: { + authHistory: { + entity: AuthHistoryEntityFixture, + }, + }, + }), + ], +}) +export class AppModuleFixture {} diff --git a/packages/nestjs-auth-history/src/__fixtures__/create-auth-history-repository.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/create-auth-history-repository.fixture.ts new file mode 100644 index 000000000..ce31da182 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/create-auth-history-repository.fixture.ts @@ -0,0 +1,73 @@ +import { DataSource, FindOneOptions } from 'typeorm'; +import { AuthHistoryEntityInterface } from '../interfaces/auth-history-entity.interface'; +import { AuthHistoryEntityFixture } from './entities/auth-history.entity.fixture'; +import { UserInterface } from '@concepta/nestjs-common'; + +export function createAuthHistoryRepositoryFixture(dataSource: DataSource) { + /** + * Fake authHistory "database" + */ + const user: UserInterface = { + id: '1', + active: false, + email: '1@example.com', + username: '1@example.com', + dateCreated: new Date(), + dateUpdated: new Date(), + dateDeleted: new Date(), + version: 1, + }; + const users: AuthHistoryEntityFixture[] = [ + { + id: '1', + userId: '1', + user: { + ...user, + id: '1', + }, + ipAddress: '127.0.0.1', + authType: 'login', + deviceInfo: 'Chrome on Windows', + dateCreated: new Date(), + dateUpdated: new Date(), + dateDeleted: new Date(), + version: 1, + }, + { + id: '2', + userId: '2', + user: { + ...user, + id: '2', + }, + ipAddress: '127.0.0.1', + authType: 'login', + deviceInfo: 'Firefox on Mac', + dateCreated: new Date(), + dateUpdated: new Date(), + dateDeleted: new Date(), + version: 1, + }, + ]; + + return dataSource.getRepository(AuthHistoryEntityFixture).extend({ + async findOne( + optionsOrConditions?: + | string + | number + | Date + // | ObjectID + | FindOneOptions, + ): Promise { + return ( + users.find((authHistory) => { + if ( + typeof optionsOrConditions === 'object' && + 'id' in optionsOrConditions + ) + return authHistory?.id === optionsOrConditions['id']; + }) ?? null + ); + }, + }); +} diff --git a/packages/nestjs-auth-history/src/__fixtures__/entities/auth-history.entity.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/entities/auth-history.entity.fixture.ts new file mode 100644 index 000000000..616717cba --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/entities/auth-history.entity.fixture.ts @@ -0,0 +1,10 @@ +import { Entity, ManyToOne } from 'typeorm'; +import { AuthHistorySqliteEntity } from '../../entities/auth-history-sqlite.entity'; +import { UserInterface } from '@concepta/nestjs-common'; +import { UserEntityFixture } from './user-entity.fixture'; + +@Entity() +export class AuthHistoryEntityFixture extends AuthHistorySqliteEntity { + @ManyToOne(() => UserEntityFixture, (user) => user.authHistory) + user!: UserInterface; +} diff --git a/packages/nestjs-auth-history/src/__fixtures__/entities/user-entity.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/entities/user-entity.fixture.ts new file mode 100644 index 000000000..b26dcded6 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/entities/user-entity.fixture.ts @@ -0,0 +1,12 @@ +import { Entity, OneToMany } from 'typeorm'; +import { UserSqliteEntity } from '@concepta/nestjs-user'; +import { AuthHistoryEntityFixture } from './auth-history.entity.fixture'; + +/** + * User Entity Fixture + */ +@Entity() +export class UserEntityFixture extends UserSqliteEntity { + @OneToMany(() => AuthHistoryEntityFixture, (authHistory) => authHistory.user) + authHistory?: AuthHistoryEntityFixture[]; +} diff --git a/packages/nestjs-auth-history/src/__fixtures__/events/invitation-accepted.event.ts b/packages/nestjs-auth-history/src/__fixtures__/events/invitation-accepted.event.ts new file mode 100644 index 000000000..a43e037a9 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/events/invitation-accepted.event.ts @@ -0,0 +1,7 @@ +import { EventAsync } from '@concepta/nestjs-event'; +import { InvitationAcceptedEventPayloadInterface } from '@concepta/nestjs-common'; + +export class InvitationAcceptedEventAsync extends EventAsync< + InvitationAcceptedEventPayloadInterface, + boolean +> {} diff --git a/packages/nestjs-auth-history/src/__fixtures__/events/invitation-get-user.event.ts b/packages/nestjs-auth-history/src/__fixtures__/events/invitation-get-user.event.ts new file mode 100644 index 000000000..683cc4f17 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/events/invitation-get-user.event.ts @@ -0,0 +1,10 @@ +import { EventAsync } from '@concepta/nestjs-event'; +import { + InvitationGetUserEventPayloadInterface, + InvitationGetUserEventResponseInterface, +} from '@concepta/nestjs-common'; + +export class InvitationGetUserEventAsync extends EventAsync< + InvitationGetUserEventPayloadInterface, + InvitationGetUserEventResponseInterface +> {} diff --git a/packages/nestjs-auth-history/src/__fixtures__/ormconfig.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/ormconfig.fixture.ts new file mode 100644 index 000000000..0d1f89ba2 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/ormconfig.fixture.ts @@ -0,0 +1,10 @@ +import { DataSourceOptions } from 'typeorm'; +import { AuthHistoryEntityFixture } from './entities/auth-history.entity.fixture'; +import { UserEntityFixture } from './entities/user-entity.fixture'; + +export const ormConfig: DataSourceOptions = { + type: 'sqlite', + database: ':memory:', + synchronize: true, + entities: [AuthHistoryEntityFixture, UserEntityFixture], +}; diff --git a/packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts b/packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts new file mode 100644 index 000000000..674db556b --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { UserInterface } from '@concepta/nestjs-common'; +import { + ReferenceEmail, + ReferenceId, + ReferenceSubject, + ReferenceUsername, +} from '@concepta/nestjs-common'; +import { AuthJwtUserLookupServiceInterface } from '@concepta/nestjs-auth-jwt/dist/interfaces/auth-jwt-user-lookup-service.interface'; + +@Injectable() +export class UserLookupCustomService + implements AuthJwtUserLookupServiceInterface +{ + async byId(id: ReferenceId): Promise { + throw new Error(`Method not implemented, cant get ${id}.`); + } + + async byEmail(email: ReferenceEmail): Promise { + throw new Error(`Method not implemented, cant get ${email}.`); + } + + async bySubject(subject: ReferenceSubject): Promise { + throw new Error(`Method not implemented, cant get ${subject}.`); + } + + async byUsername(username: ReferenceUsername): Promise { + throw new Error(`Method not implemented, cant get ${username}.`); + } +} diff --git a/packages/nestjs-auth-history/src/auth-history.constants.ts b/packages/nestjs-auth-history/src/auth-history.constants.ts new file mode 100644 index 000000000..a759078ca --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.constants.ts @@ -0,0 +1,11 @@ +export const AUTH_HISTORY_MODULE_OPTIONS_TOKEN = + 'AUTH_HISTORY_MODULE_OPTIONS_TOKEN'; +export const AUTH_HISTORY_MODULE_SETTINGS_TOKEN = + 'AUTH_HISTORY_MODULE_SETTINGS_TOKEN'; +export const AUTH_HISTORY_MODULE_DEFAULT_SETTINGS_TOKEN = + 'AUTH_HISTORY_MODULE_DEFAULT_SETTINGS_TOKEN'; +export const AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY = 'authHistory'; +export const AUTH_HISTORY_MODULE_AUTH_HISTORY_PASSWORD_HISTORY_ENTITY_KEY = + 'auth-history-password-history'; +export const AUTH_HISTORY_MODULE_AUTH_HISTORY_PASSWORD_HISTORY_LIMIT_DAYS_DEFAULT = + 365 * 2; diff --git a/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts b/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts new file mode 100644 index 000000000..d8ed3ecfb --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts @@ -0,0 +1,123 @@ +import { + AccessControlFilter, + AccessControlGuard, +} from '@concepta/nestjs-access-control'; +import { SeedingSource } from '@concepta/typeorm-seeding'; +import { + CallHandler, + ExecutionContext, + INestApplication, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import supertest from 'supertest'; + +import { AuthHistoryFactory } from './auth-history.factory'; + +import { UserInterface } from '@concepta/nestjs-common'; +import { UserFactory } from '@concepta/nestjs-user/src/user.factory'; +import { AppModuleFixture } from './__fixtures__/app.module.fixture'; +import { AuthHistoryEntityFixture } from './__fixtures__/entities/auth-history.entity.fixture'; +import { UserEntityFixture } from './__fixtures__/entities/user-entity.fixture'; + +describe('AuthHistoryController (e2e)', () => { + describe('Normal CRUD flow', () => { + let app: INestApplication; + let seedingSource: SeedingSource; + let user: UserInterface; + let accessControlGuard: AccessControlGuard; + let accessControlFilter: AccessControlFilter; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModuleFixture], + }).compile(); + app = moduleFixture.createNestApplication(); + await app.init(); + + // authJwtGuard = app.get(AuthJwtGuard); + // jest.spyOn(authJwtGuard, 'canActivate').mockResolvedValue(true); + + accessControlGuard = app.get(AccessControlGuard); + jest.spyOn(accessControlGuard, 'canActivate').mockResolvedValue(true); + + accessControlFilter = app.get(AccessControlFilter); + jest + .spyOn(accessControlFilter, 'intercept') + .mockImplementation((_context: ExecutionContext, next: CallHandler) => { + return Promise.resolve(next.handle()); + }); + + seedingSource = new SeedingSource({ + dataSource: app.get(getDataSourceToken()), + }); + + await seedingSource.initialize(); + + const userFactory = new UserFactory({ + entity: UserEntityFixture, + seedingSource, + }); + + user = await userFactory.create(); + + const authHistoryFactory = new AuthHistoryFactory({ + entity: AuthHistoryEntityFixture, + seedingSource, + }); + + await authHistoryFactory.createMany(2, { + user, + userId: user.id, + }); + }); + + afterEach(async () => { + jest.clearAllMocks(); + return app ? await app.close() : undefined; + }); + + it('GET /auth-history', async () => { + await supertest(app.getHttpServer()) + .get('/auth-history?limit=10') + .expect(200); + }); + + it('GET /auth-history/:id', async () => { + // get a login history so we have an id + const response = await supertest(app.getHttpServer()) + .get('/auth-history?limit=1') + .expect(200); + + // get one using that id + await supertest(app.getHttpServer()) + .get(`/auth-history/${response.body[0].id}`) + .expect(200); + }); + + it('POST /auth-history', async () => { + await supertest(app.getHttpServer()) + .post('/auth-history') + .send({ + ipAddress: '127:0:0:1', + authType: '2FA', + deviceInfo: 'IOS', + user, + userId: user.id, + }) + .expect(201); + }); + + it('DELETE /auth-history/:id', async () => { + // get a login history so we have an id + const response = await supertest(app.getHttpServer()) + .get('/auth-history?limit=1') + .expect(200); + + // delete one using that id + await supertest(app.getHttpServer()) + .delete(`/auth-history/${response.body[0].id}`) + .expect(200); + }); + }); +}); diff --git a/packages/nestjs-auth-history/src/auth-history.controller.ts b/packages/nestjs-auth-history/src/auth-history.controller.ts new file mode 100644 index 000000000..729256b8b --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.controller.ts @@ -0,0 +1,107 @@ +import { + AccessControlCreateOne, + AccessControlDeleteOne, + AccessControlReadMany, + AccessControlReadOne, +} from '@concepta/nestjs-access-control'; +import { AuthHistoryCreatableInterface } from '@concepta/nestjs-common'; +import { + CrudBody, + CrudController, + CrudControllerInterface, + CrudCreateOne, + CrudDeleteOne, + CrudReadMany, + CrudReadOne, + CrudRequest, + CrudRequestInterface, +} from '@concepta/nestjs-crud'; +import { ApiTags } from '@nestjs/swagger'; + +import { AuthHistoryResource } from './auth-history.types'; +import { AuthHistoryCreateDto } from './dto/auth-history-create.dto'; +import { AuthHistoryPaginatedDto } from './dto/auth-history-paginated.dto'; +import { AuthHistoryDto } from './dto/auth-history.dto'; +import { AuthHistoryEntityInterface } from './interfaces/auth-history-entity.interface'; + +import { AuthHistoryCrudService } from './services/auth-history-crud.service'; + +/** + * AuthHistory controller. + */ +@CrudController({ + path: 'auth-history', + model: { + type: AuthHistoryDto, + paginatedType: AuthHistoryPaginatedDto, + }, +}) +@ApiTags('auth-history') +export class AuthHistoryController + implements + CrudControllerInterface< + AuthHistoryEntityInterface, + AuthHistoryCreatableInterface, + never + > +{ + /** + * Constructor. + * + * @param authHistoryCrudService - Instance of the auth history CRUD service + * that handles basic CRUD operations + */ + constructor(private authHistoryCrudService: AuthHistoryCrudService) {} + + /** + * Get many + * + * @param crudRequest - the CRUD request object + */ + @CrudReadMany() + @AccessControlReadMany(AuthHistoryResource.Many) + async getMany(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.authHistoryCrudService.getMany(crudRequest); + } + + /** + * Get one + * + * @param crudRequest - the CRUD request object + */ + @CrudReadOne() + @AccessControlReadOne(AuthHistoryResource.One) + async getOne(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.authHistoryCrudService.getOne(crudRequest); + } + + /** + * Create one + * + * @param crudRequest - the CRUD request object + * @param authHistoryCreateDto - auth history create dto + */ + @CrudCreateOne() + @AccessControlCreateOne(AuthHistoryResource.One) + async createOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() authHistoryCreateDto: AuthHistoryCreateDto, + ) { + // call crud service to create + return this.authHistoryCrudService.createOne( + crudRequest, + authHistoryCreateDto, + ); + } + + /** + * Delete one + * + * @param crudRequest - the CRUD request object + */ + @CrudDeleteOne() + @AccessControlDeleteOne(AuthHistoryResource.One) + async deleteOne(@CrudRequest() crudRequest: CrudRequestInterface) { + return this.authHistoryCrudService.deleteOne(crudRequest); + } +} diff --git a/packages/nestjs-auth-history/src/auth-history.factory.ts b/packages/nestjs-auth-history/src/auth-history.factory.ts new file mode 100644 index 000000000..7308d11d8 --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.factory.ts @@ -0,0 +1,36 @@ +import { Factory } from '@concepta/typeorm-seeding'; +import { faker } from '@faker-js/faker'; +import { AuthHistoryEntityInterface } from './interfaces/auth-history-entity.interface'; +/** + * AuthHistory factory + */ +export class AuthHistoryFactory extends Factory { + /** + * Factory callback function. + */ + protected async entity( + authHistory: AuthHistoryEntityInterface, + ): Promise { + // set random ip address (IPv4 format) + authHistory.ipAddress = faker.internet.ip(); + + // set random auth type + authHistory.authType = faker.helpers.arrayElement([ + 'login', + 'logout', + 'register', + 'password-reset', + ]); + + // set random device info + authHistory.deviceInfo = `${faker.helpers.arrayElement([ + 'Chrome', + 'Firefox', + 'Safari', + 'Edge', + ])}}`; + + // return the new authHistory + return authHistory; + } +} diff --git a/packages/nestjs-auth-history/src/auth-history.module-definition.ts b/packages/nestjs-auth-history/src/auth-history.module-definition.ts new file mode 100644 index 000000000..bc05839e1 --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.module-definition.ts @@ -0,0 +1,130 @@ +import { createSettingsProvider } from '@concepta/nestjs-common'; +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; +import { + ConfigurableModuleBuilder, + DynamicModule, + Provider, +} from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { AUTH_HISTORY_MODULE_SETTINGS_TOKEN } from './auth-history.constants'; + +import { AuthHistoryEntitiesOptionsInterface } from './interfaces/auth-history-entities-options.interface'; +import { AuthHistoryOptionsExtrasInterface } from './interfaces/auth-history-options-extras.interface'; +import { AuthHistoryOptionsInterface } from './interfaces/auth-history-options.interface'; +import { AuthHistorySettingsInterface } from './interfaces/auth-history-settings.interface'; + +import { AuthHistoryController } from './auth-history.controller'; +import { AuthHistoryCrudService } from './services/auth-history-crud.service'; + +import { authHistoryDefaultConfig } from './config/auth-history-default.config'; +import { AuthHistoryAccessQueryService } from './services/auth-history-query.service'; + +const RAW_OPTIONS_TOKEN = Symbol('__AUTH_HISTORY_MODULE_RAW_OPTIONS_TOKEN__'); + +export const { + ConfigurableModuleClass: AuthHistoryModuleClass, + OPTIONS_TYPE: AUTH_HISTORY_OPTIONS_TYPE, + ASYNC_OPTIONS_TYPE: AuthHistory_ASYNC_OPTIONS_TYPE, +} = new ConfigurableModuleBuilder({ + moduleName: 'AuthHistory', + optionsInjectionToken: RAW_OPTIONS_TOKEN, +}) + .setExtras( + { global: false }, + definitionTransform, + ) + .build(); + +export type AuthHistoryOptions = Omit< + typeof AUTH_HISTORY_OPTIONS_TYPE, + 'global' +>; +export type AuthHistoryAsyncOptions = Omit< + typeof AuthHistory_ASYNC_OPTIONS_TYPE, + 'global' +>; + +function definitionTransform( + definition: DynamicModule, + extras: AuthHistoryOptionsExtrasInterface, +): DynamicModule { + const { providers = [], imports = [] } = definition; + const { controllers, global = false, entities } = extras; + + if (!entities) { + throw new Error('You must provide the entities option'); + } + + return { + ...definition, + global, + imports: createAuthHistoryImports({ imports, entities }), + providers: createAuthHistoryProviders({ providers }), + controllers: createAuthHistoryControllers({ controllers }), + exports: [ConfigModule, RAW_OPTIONS_TOKEN, ...createAuthHistoryExports()], + }; +} + +export function createAuthHistoryImports( + options: Pick & AuthHistoryEntitiesOptionsInterface, +): Required>['imports'] { + return [ + ...(options.imports ?? []), + ConfigModule.forFeature(authHistoryDefaultConfig), + TypeOrmExtModule.forFeature(options.entities), + ]; +} + +export function createAuthHistoryProviders(options: { + overrides?: AuthHistoryOptions; + providers?: Provider[]; +}): Provider[] { + return [ + ...(options.providers ?? []), + AuthHistoryCrudService, + createAuthHistorySettingsProvider(options.overrides), + createAuthHistoryAccessQueryServiceProvider(options.overrides), + ]; +} + +export function createAuthHistoryExports(): Required< + Pick +>['exports'] { + return [AUTH_HISTORY_MODULE_SETTINGS_TOKEN, AuthHistoryCrudService]; +} + +export function createAuthHistoryControllers( + overrides: Pick = {}, +): DynamicModule['controllers'] { + return overrides?.controllers !== undefined + ? overrides.controllers + : [AuthHistoryController]; +} + +export function createAuthHistorySettingsProvider( + optionsOverrides?: AuthHistoryOptions, +): Provider { + return createSettingsProvider< + AuthHistorySettingsInterface, + AuthHistoryOptionsInterface + >({ + settingsToken: AUTH_HISTORY_MODULE_SETTINGS_TOKEN, + optionsToken: RAW_OPTIONS_TOKEN, + settingsKey: authHistoryDefaultConfig.KEY, + optionsOverrides, + }); +} + +export function createAuthHistoryAccessQueryServiceProvider( + optionsOverrides?: AuthHistoryOptions, +): Provider { + return { + provide: AuthHistoryAccessQueryService, + inject: [RAW_OPTIONS_TOKEN], + useFactory: async (options: AuthHistoryOptionsInterface) => + optionsOverrides?.authHistoryAccessQueryService ?? + options.authHistoryAccessQueryService ?? + new AuthHistoryAccessQueryService(), + }; +} diff --git a/packages/nestjs-auth-history/src/auth-history.module.spec.ts b/packages/nestjs-auth-history/src/auth-history.module.spec.ts new file mode 100644 index 000000000..9f31711ca --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.module.spec.ts @@ -0,0 +1,48 @@ +import { getDynamicRepositoryToken } from '@concepta/nestjs-typeorm-ext'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Repository } from 'typeorm'; + +import { AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY } from './auth-history.constants'; +import { AuthHistoryController } from './auth-history.controller'; +import { AuthHistoryModule } from './auth-history.module'; +import { AuthHistoryCrudService } from './services/auth-history-crud.service'; + +import { AppModuleFixture } from './__fixtures__/app.module.fixture'; +import { AuthHistoryEntityFixture } from './__fixtures__/entities/auth-history.entity.fixture'; + +describe('AppModule', () => { + let authHistoryModule: AuthHistoryModule; + let authHistoryCrudService: AuthHistoryCrudService; + let authHistoryController: AuthHistoryController; + let authHistoryRepo: Repository; + + beforeEach(async () => { + const testModule: TestingModule = await Test.createTestingModule({ + imports: [AppModuleFixture], + }).compile(); + + authHistoryModule = testModule.get(AuthHistoryModule); + authHistoryRepo = testModule.get( + getDynamicRepositoryToken(AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY), + ); + authHistoryCrudService = testModule.get( + AuthHistoryCrudService, + ); + authHistoryController = testModule.get( + AuthHistoryController, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('module', () => { + it('should be loaded', async () => { + expect(authHistoryModule).toBeInstanceOf(AuthHistoryModule); + expect(authHistoryRepo).toBeInstanceOf(Repository); + expect(authHistoryCrudService).toBeInstanceOf(AuthHistoryCrudService); + expect(authHistoryController).toBeInstanceOf(AuthHistoryController); + }); + }); +}); diff --git a/packages/nestjs-auth-history/src/auth-history.module.ts b/packages/nestjs-auth-history/src/auth-history.module.ts new file mode 100644 index 000000000..d91b23da0 --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.module.ts @@ -0,0 +1,29 @@ +import { DynamicModule, Module } from '@nestjs/common'; + +import { + AuthHistoryAsyncOptions, + AuthHistoryModuleClass, + AuthHistoryOptions, +} from './auth-history.module-definition'; + +/** + * AuthHistory Module + */ +@Module({}) +export class AuthHistoryModule extends AuthHistoryModuleClass { + static register(options: AuthHistoryOptions): DynamicModule { + return super.register(options); + } + + static registerAsync(options: AuthHistoryAsyncOptions): DynamicModule { + return super.registerAsync(options); + } + + static forRoot(options: AuthHistoryOptions): DynamicModule { + return super.register({ ...options, global: true }); + } + + static forRootAsync(options: AuthHistoryAsyncOptions): DynamicModule { + return super.registerAsync({ ...options, global: true }); + } +} diff --git a/packages/nestjs-auth-history/src/auth-history.seeder.ts b/packages/nestjs-auth-history/src/auth-history.seeder.ts new file mode 100644 index 000000000..92e0f6003 --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.seeder.ts @@ -0,0 +1,34 @@ +import { Seeder } from '@concepta/typeorm-seeding'; +import { UserFactory } from '@concepta/nestjs-user/src/seeding'; +import { AuthHistoryFactory } from './auth-history.factory'; +import { UserEntityFixture } from './__fixtures__/entities/user-entity.fixture'; +/** + * AuthHistory seeder + */ +export class AuthHistorySeeder extends Seeder { + /** + * Runner + */ + public async run(): Promise { + // number of authHistorys to create + const createAmount = process.env?.AUTH_HISTORY_MODULE_SEEDER_AMOUNT + ? Number(process.env.AUTH_HISTORY_MODULE_SEEDER_AMOUNT) + : 50; + + this.factory(UserFactory); + // the factory + const authHistoryFactory = this.factory(AuthHistoryFactory); + + const userFactory = new UserFactory({ + entity: UserEntityFixture, + }); + + const user = await userFactory.create(); + + // create a bunch more + await authHistoryFactory.createMany(createAmount, { + user: user, + userId: user.id, + }); + } +} diff --git a/packages/nestjs-auth-history/src/auth-history.types.spec.ts b/packages/nestjs-auth-history/src/auth-history.types.spec.ts new file mode 100644 index 000000000..de7cc1e5a --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.types.spec.ts @@ -0,0 +1,10 @@ +import { AuthHistoryResource } from './auth-history.types'; + +describe('AuthHistory Types', () => { + describe('AuthHistoryResource enum', () => { + it('should match', async () => { + expect(AuthHistoryResource.One).toEqual('auth-history'); + expect(AuthHistoryResource.Many).toEqual('auth-history-list'); + }); + }); +}); diff --git a/packages/nestjs-auth-history/src/auth-history.types.ts b/packages/nestjs-auth-history/src/auth-history.types.ts new file mode 100644 index 000000000..078b8b321 --- /dev/null +++ b/packages/nestjs-auth-history/src/auth-history.types.ts @@ -0,0 +1,4 @@ +export enum AuthHistoryResource { + 'One' = 'auth-history', + 'Many' = 'auth-history-list', +} diff --git a/packages/nestjs-auth-history/src/config/auth-history-default.config.ts b/packages/nestjs-auth-history/src/config/auth-history-default.config.ts new file mode 100644 index 000000000..87240ad28 --- /dev/null +++ b/packages/nestjs-auth-history/src/config/auth-history-default.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; +import { AUTH_HISTORY_MODULE_DEFAULT_SETTINGS_TOKEN } from '../auth-history.constants'; +import { AuthHistorySettingsInterface } from '../interfaces/auth-history-settings.interface'; + +/** + * Default configuration for AuthHistory module. + */ +export const authHistoryDefaultConfig = registerAs( + AUTH_HISTORY_MODULE_DEFAULT_SETTINGS_TOKEN, + (): AuthHistorySettingsInterface => ({}), +); diff --git a/packages/nestjs-auth-history/src/dto/auth-history-create.dto.ts b/packages/nestjs-auth-history/src/dto/auth-history-create.dto.ts new file mode 100644 index 000000000..9fdfdf7e0 --- /dev/null +++ b/packages/nestjs-auth-history/src/dto/auth-history-create.dto.ts @@ -0,0 +1,20 @@ +import { Exclude } from 'class-transformer'; +import { IntersectionType, PickType } from '@nestjs/swagger'; +import { AuthHistoryCreatableInterface } from '@concepta/nestjs-common'; +import { AuthHistoryDto } from './auth-history.dto'; + +/** + * AuthHistory Create DTO + */ +@Exclude() +export class AuthHistoryCreateDto + extends IntersectionType( + PickType(AuthHistoryDto, [ + 'userId', + 'authType', + 'ipAddress', + 'deviceInfo', + ] as const), + PickType(AuthHistoryDto, ['deviceInfo'] as const), + ) + implements AuthHistoryCreatableInterface {} diff --git a/packages/nestjs-auth-history/src/dto/auth-history-paginated.dto.ts b/packages/nestjs-auth-history/src/dto/auth-history-paginated.dto.ts new file mode 100644 index 000000000..bc364fde0 --- /dev/null +++ b/packages/nestjs-auth-history/src/dto/auth-history-paginated.dto.ts @@ -0,0 +1,20 @@ +import { Exclude, Expose, Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; +import { AuthHistoryInterface } from '@concepta/nestjs-common'; +import { CrudResponsePaginatedDto } from '@concepta/nestjs-crud'; +import { AuthHistoryDto } from './auth-history.dto'; + +/** + * AuthHistory paginated DTO + */ +@Exclude() +export class AuthHistoryPaginatedDto extends CrudResponsePaginatedDto { + @Expose() + @ApiProperty({ + type: AuthHistoryDto, + isArray: true, + description: 'Array of AuthHistorys', + }) + @Type(() => AuthHistoryDto) + data: AuthHistoryDto[] = []; +} diff --git a/packages/nestjs-auth-history/src/dto/auth-history.dto.ts b/packages/nestjs-auth-history/src/dto/auth-history.dto.ts new file mode 100644 index 000000000..8ea85b1c9 --- /dev/null +++ b/packages/nestjs-auth-history/src/dto/auth-history.dto.ts @@ -0,0 +1,57 @@ +import { AuthHistoryInterface, CommonEntityDto } from '@concepta/nestjs-common'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Exclude, Expose } from 'class-transformer'; +import { IsOptional, IsString } from 'class-validator'; + +/** + * AuthHistory DTO + */ +@Exclude() +export class AuthHistoryDto + extends CommonEntityDto + implements AuthHistoryInterface +{ + /** + * User ID + */ + @Expose() + @ApiProperty({ + type: 'string', + description: 'User ID', + }) + @IsString() + userId: string = ''; + + /** + * IP Address + */ + @Expose() + @ApiProperty({ + type: 'string', + description: 'IP Address', + }) + @IsString() + ipAddress: string = ''; + + /** + * Auth Type + */ + @Expose() + @ApiProperty({ + type: 'string', + description: 'Auth Type', + }) + @IsString() + authType: string = ''; + + /** + * Device Info + */ + @Expose() + @ApiPropertyOptional({ + type: 'string', + description: 'Device Info', + }) + @IsOptional() + deviceInfo?: string; +} diff --git a/packages/nestjs-auth-history/src/entities/auth-history-postgres.entity.ts b/packages/nestjs-auth-history/src/entities/auth-history-postgres.entity.ts new file mode 100644 index 000000000..f57fe941e --- /dev/null +++ b/packages/nestjs-auth-history/src/entities/auth-history-postgres.entity.ts @@ -0,0 +1,41 @@ +import { Column } from 'typeorm'; +import { CommonPostgresEntity } from '@concepta/typeorm-common'; +import { ReferenceId, UserInterface } from '@concepta/nestjs-common'; +import { AuthHistoryEntityInterface } from '../interfaces/auth-history-entity.interface'; + +/** + * AuthHistory Entity + */ +export abstract class AuthHistoryPostgresEntity + extends CommonPostgresEntity + implements AuthHistoryEntityInterface +{ + /** + * ipAddress + */ + @Column({ type: 'text' }) + ipAddress!: string; + + /** + * authType + */ + @Column({ type: 'citext' }) + authType!: string; + + /** + * deviceInfo + */ + @Column({ type: 'citext' }) + deviceInfo!: string; + + /** + * User ID + */ + @Column({ type: 'uuid' }) + userId!: ReferenceId; + + /** + * Should be configured by the implementation + */ + user?: UserInterface; +} diff --git a/packages/nestjs-auth-history/src/entities/auth-history-sqlite.entity.ts b/packages/nestjs-auth-history/src/entities/auth-history-sqlite.entity.ts new file mode 100644 index 000000000..e6920dd44 --- /dev/null +++ b/packages/nestjs-auth-history/src/entities/auth-history-sqlite.entity.ts @@ -0,0 +1,38 @@ +import { ReferenceId, UserInterface } from '@concepta/nestjs-common'; +import { CommonSqliteEntity } from '@concepta/typeorm-common'; +import { Column } from 'typeorm'; +import { AuthHistoryEntityInterface } from '../interfaces/auth-history-entity.interface'; + +export abstract class AuthHistorySqliteEntity + extends CommonSqliteEntity + implements AuthHistoryEntityInterface +{ + /** + * ipAddress + */ + @Column({ type: 'text' }) + ipAddress!: string; + + /** + * authType + */ + @Column({ type: 'text' }) + authType!: string; + + /** + * deviceInfo + */ + @Column({ type: 'text' }) + deviceInfo!: string; + + /** + * User ID + */ + @Column({ type: 'uuid' }) + userId!: ReferenceId; + + /** + * Should be configured by the implementation + */ + user?: UserInterface; +} diff --git a/packages/nestjs-auth-history/src/exceptions/auth-history-bad-request-exception.ts b/packages/nestjs-auth-history/src/exceptions/auth-history-bad-request-exception.ts new file mode 100644 index 000000000..99e6533af --- /dev/null +++ b/packages/nestjs-auth-history/src/exceptions/auth-history-bad-request-exception.ts @@ -0,0 +1,14 @@ +import { RuntimeExceptionOptions } from '@concepta/nestjs-exception'; +import { HttpStatus } from '@nestjs/common'; +import { AuthHistoryException } from './auth-history-exception'; + +export class AuthHistoryBadRequestException extends AuthHistoryException { + constructor(options?: RuntimeExceptionOptions) { + super({ + httpStatus: HttpStatus.BAD_REQUEST, + ...options, + }); + + this.errorCode = 'AUTH_HISTORY_BAD_REQUEST_ERROR'; + } +} diff --git a/packages/nestjs-auth-history/src/exceptions/auth-history-exception.ts b/packages/nestjs-auth-history/src/exceptions/auth-history-exception.ts new file mode 100644 index 000000000..5bbb1b808 --- /dev/null +++ b/packages/nestjs-auth-history/src/exceptions/auth-history-exception.ts @@ -0,0 +1,13 @@ +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; +/** + * Generic login history exception. + */ +export class AuthHistoryException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super(options); + this.errorCode = 'AUTH_HISTORY_ERROR'; + } +} diff --git a/packages/nestjs-auth-history/src/exceptions/auth-history-not-found-exception.ts b/packages/nestjs-auth-history/src/exceptions/auth-history-not-found-exception.ts new file mode 100644 index 000000000..4bd818b95 --- /dev/null +++ b/packages/nestjs-auth-history/src/exceptions/auth-history-not-found-exception.ts @@ -0,0 +1,15 @@ +import { RuntimeExceptionOptions } from '@concepta/nestjs-exception'; +import { HttpStatus } from '@nestjs/common'; +import { AuthHistoryException } from './auth-history-exception'; + +export class AuthHistoryNotFoundException extends AuthHistoryException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'The login history was not found', + httpStatus: HttpStatus.NOT_FOUND, + ...options, + }); + + this.errorCode = 'AUTH_HISTORY_NOT_FOUND_ERROR'; + } +} diff --git a/packages/nestjs-auth-history/src/index.ts b/packages/nestjs-auth-history/src/index.ts new file mode 100644 index 000000000..376f043d1 --- /dev/null +++ b/packages/nestjs-auth-history/src/index.ts @@ -0,0 +1,21 @@ +export { AuthHistoryModule } from './auth-history.module'; + +export { AuthHistoryPostgresEntity } from './entities/auth-history-postgres.entity'; +export { AuthHistorySqliteEntity } from './entities/auth-history-sqlite.entity'; + +export { AuthHistoryController } from './auth-history.controller'; +export { AuthHistoryAccessQueryService } from './services/auth-history-query.service'; + +export { AuthHistoryCrudService } from './services/auth-history-crud.service'; + +export { AuthHistoryEntityInterface } from './interfaces/auth-history-entity.interface'; + +export { AuthHistoryCreateDto } from './dto/auth-history-create.dto'; +export { AuthHistoryPaginatedDto } from './dto/auth-history-paginated.dto'; +export { AuthHistoryDto } from './dto/auth-history.dto'; + +export { AuthHistoryResource } from './auth-history.types'; + +export { AuthHistoryBadRequestException } from './exceptions/auth-history-bad-request-exception'; +export { AuthHistoryException } from './exceptions/auth-history-exception'; +export { AuthHistoryNotFoundException } from './exceptions/auth-history-not-found-exception'; diff --git a/packages/nestjs-auth-history/src/interfaces/auth-history-entities-options.interface.ts b/packages/nestjs-auth-history/src/interfaces/auth-history-entities-options.interface.ts new file mode 100644 index 000000000..35ceec4f3 --- /dev/null +++ b/packages/nestjs-auth-history/src/interfaces/auth-history-entities-options.interface.ts @@ -0,0 +1,9 @@ +import { TypeOrmExtEntityOptionInterface } from '@concepta/nestjs-typeorm-ext'; +import { AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY } from '../auth-history.constants'; +import { AuthHistoryEntityInterface } from './auth-history-entity.interface'; + +export interface AuthHistoryEntitiesOptionsInterface { + entities: { + [AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY]: TypeOrmExtEntityOptionInterface; + }; +} diff --git a/packages/nestjs-auth-history/src/interfaces/auth-history-entity.interface.ts b/packages/nestjs-auth-history/src/interfaces/auth-history-entity.interface.ts new file mode 100644 index 000000000..ec62e5fc3 --- /dev/null +++ b/packages/nestjs-auth-history/src/interfaces/auth-history-entity.interface.ts @@ -0,0 +1,8 @@ +import { + AuthHistoryInterface, + ReferenceIdInterface, +} from '@concepta/nestjs-common'; + +export interface AuthHistoryEntityInterface + extends ReferenceIdInterface, + AuthHistoryInterface {} diff --git a/packages/nestjs-auth-history/src/interfaces/auth-history-mutate-service.interface.ts b/packages/nestjs-auth-history/src/interfaces/auth-history-mutate-service.interface.ts new file mode 100644 index 000000000..2f740ca53 --- /dev/null +++ b/packages/nestjs-auth-history/src/interfaces/auth-history-mutate-service.interface.ts @@ -0,0 +1,11 @@ +import { + AuthHistoryCreatableInterface, + CreateOneInterface, +} from '@concepta/nestjs-common'; +import { AuthHistoryEntityInterface } from './auth-history-entity.interface'; + +export interface AuthHistoryMutateServiceInterface + extends CreateOneInterface< + AuthHistoryCreatableInterface, + AuthHistoryEntityInterface + > {} diff --git a/packages/nestjs-auth-history/src/interfaces/auth-history-options-extras.interface.ts b/packages/nestjs-auth-history/src/interfaces/auth-history-options-extras.interface.ts new file mode 100644 index 000000000..58b11bdde --- /dev/null +++ b/packages/nestjs-auth-history/src/interfaces/auth-history-options-extras.interface.ts @@ -0,0 +1,6 @@ +import { DynamicModule } from '@nestjs/common'; +import { AuthHistoryEntitiesOptionsInterface } from './auth-history-entities-options.interface'; + +export interface AuthHistoryOptionsExtrasInterface + extends Pick, + Partial {} diff --git a/packages/nestjs-auth-history/src/interfaces/auth-history-options.interface.ts b/packages/nestjs-auth-history/src/interfaces/auth-history-options.interface.ts new file mode 100644 index 000000000..78024640b --- /dev/null +++ b/packages/nestjs-auth-history/src/interfaces/auth-history-options.interface.ts @@ -0,0 +1,7 @@ +import { CanAccess } from '@concepta/nestjs-access-control'; +import { AuthHistorySettingsInterface } from './auth-history-settings.interface'; + +export interface AuthHistoryOptionsInterface { + settings?: AuthHistorySettingsInterface; + authHistoryAccessQueryService?: CanAccess; +} diff --git a/packages/nestjs-auth-history/src/interfaces/auth-history-settings.interface.ts b/packages/nestjs-auth-history/src/interfaces/auth-history-settings.interface.ts new file mode 100644 index 000000000..82a085f47 --- /dev/null +++ b/packages/nestjs-auth-history/src/interfaces/auth-history-settings.interface.ts @@ -0,0 +1,11 @@ +import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; +import { + EventAsyncInterface, + EventClassInterface, +} from '@concepta/nestjs-event'; + +export interface AuthHistorySettingsInterface { + authenticatedEvents?: EventClassInterface< + EventAsyncInterface + >[]; +} diff --git a/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts b/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts new file mode 100644 index 000000000..1173476a3 --- /dev/null +++ b/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts @@ -0,0 +1,65 @@ +import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; +import { + EventAsyncInterface, + EventListenerOn, + EventListenService, +} from '@concepta/nestjs-event'; +import { + Inject, + Injectable, + Logger, + OnModuleInit, + Optional, +} from '@nestjs/common'; +import { AUTH_HISTORY_MODULE_SETTINGS_TOKEN } from '../auth-history.constants'; +import { AuthHistoryMutateServiceInterface } from '../interfaces/auth-history-mutate-service.interface'; +import { AuthHistorySettingsInterface } from '../interfaces/auth-history-settings.interface'; +import { AuthHistoryMutateService } from '../services/auth-history-mutate.service'; + +@Injectable() +export class AuthenticatedListener + extends EventListenerOn< + EventAsyncInterface + > + implements OnModuleInit +{ + constructor( + @Inject(AUTH_HISTORY_MODULE_SETTINGS_TOKEN) + private settings: AuthHistorySettingsInterface, + @Inject(AuthHistoryMutateService) + private authHistoryMutateService: AuthHistoryMutateServiceInterface, + @Optional() + @Inject(EventListenService) + private eventListenService?: EventListenService, + ) { + super(); + } + + onModuleInit() { + if (this.eventListenService && this.settings.authenticatedEvents) { + this.settings.authenticatedEvents.forEach((event) => { + this.eventListenService?.on(event, this); + }); + } + } + + async listen( + event: EventAsyncInterface, + ) { + const { payload } = event; + + if (!payload.userInfo) { + Logger.error('Auth History event data payload is missing.'); + return false; + } + try { + await this.authHistoryMutateService.create( + payload.userInfo, + payload.queryOptions, + ); + } catch (err) { + Logger.error(err); + } + return true; + } +} diff --git a/packages/nestjs-auth-history/src/seeding.ts b/packages/nestjs-auth-history/src/seeding.ts new file mode 100644 index 000000000..ccec2f424 --- /dev/null +++ b/packages/nestjs-auth-history/src/seeding.ts @@ -0,0 +1,7 @@ +/** + * These exports all you to import seeding related classes + * and tools without loading the entire module which + * runs all of it's decorators and meta data. + */ +export { AuthHistoryFactory } from './auth-history.factory'; +export { AuthHistorySeeder } from './auth-history.seeder'; diff --git a/packages/nestjs-auth-history/src/services/auth-history-crud.service.ts b/packages/nestjs-auth-history/src/services/auth-history-crud.service.ts new file mode 100644 index 000000000..685e688a0 --- /dev/null +++ b/packages/nestjs-auth-history/src/services/auth-history-crud.service.ts @@ -0,0 +1,24 @@ +import { Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { TypeOrmCrudService } from '@concepta/nestjs-crud'; +import { InjectDynamicRepository } from '@concepta/nestjs-typeorm-ext'; +import { AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY } from '../auth-history.constants'; +import { AuthHistoryEntityInterface } from '../interfaces/auth-history-entity.interface'; + +/** + * AuthHistory CRUD service + */ +@Injectable() +export class AuthHistoryCrudService extends TypeOrmCrudService { + /** + * Constructor + * + * @param authHistoryRepo - instance of the login history repository. + */ + constructor( + @InjectDynamicRepository(AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY) + protected readonly authHistoryRepo: Repository, + ) { + super(authHistoryRepo); + } +} diff --git a/packages/nestjs-auth-history/src/services/auth-history-mutate.service.ts b/packages/nestjs-auth-history/src/services/auth-history-mutate.service.ts new file mode 100644 index 000000000..7bf67e3c9 --- /dev/null +++ b/packages/nestjs-auth-history/src/services/auth-history-mutate.service.ts @@ -0,0 +1,33 @@ +import { AuthHistoryCreatableInterface } from '@concepta/nestjs-common'; +import { InjectDynamicRepository } from '@concepta/nestjs-typeorm-ext'; +import { MutateService } from '@concepta/typeorm-common'; +import { Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; + +import { AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY } from '../auth-history.constants'; +import { AuthHistoryCreateDto } from '../dto/auth-history-create.dto'; +import { AuthHistoryEntityInterface } from '../interfaces/auth-history-entity.interface'; +import { AuthHistoryMutateServiceInterface } from '../interfaces/auth-history-mutate-service.interface'; + +/** + * AuthHistory mutate service + */ +@Injectable() +export class AuthHistoryMutateService + extends MutateService< + AuthHistoryEntityInterface, + AuthHistoryCreatableInterface, + AuthHistoryCreatableInterface + > + implements AuthHistoryMutateServiceInterface +{ + protected createDto = AuthHistoryCreateDto; + protected updateDto = AuthHistoryCreateDto; + + constructor( + @InjectDynamicRepository(AUTH_HISTORY_MODULE_AUTH_HISTORY_ENTITY_KEY) + repo: Repository, + ) { + super(repo); + } +} diff --git a/packages/nestjs-auth-history/src/services/auth-history-query.service.ts b/packages/nestjs-auth-history/src/services/auth-history-query.service.ts new file mode 100644 index 000000000..08d35ec33 --- /dev/null +++ b/packages/nestjs-auth-history/src/services/auth-history-query.service.ts @@ -0,0 +1,13 @@ +import { + AccessControlContext, + CanAccess, +} from '@concepta/nestjs-access-control'; +import { Injectable } from '@nestjs/common'; + +// TODO: check if this is actually needed +@Injectable() +export class AuthHistoryAccessQueryService implements CanAccess { + async canAccess(_context: AccessControlContext): Promise { + return true; + } +} diff --git a/packages/nestjs-auth-history/tsconfig.json b/packages/nestjs-auth-history/tsconfig.json new file mode 100644 index 000000000..0fe61fc61 --- /dev/null +++ b/packages/nestjs-auth-history/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist", + "typeRoots": [ + "./node_modules/@types", + "../../node_modules/@types" + ] + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../nestjs-crud" + } + ] +} diff --git a/packages/nestjs-auth-history/typedoc.json b/packages/nestjs-auth-history/typedoc.json new file mode 100644 index 000000000..944fda5ad --- /dev/null +++ b/packages/nestjs-auth-history/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} \ No newline at end of file diff --git a/packages/nestjs-auth-local/package.json b/packages/nestjs-auth-local/package.json index a2a16c3d2..430dfbdfa 100644 --- a/packages/nestjs-auth-local/package.json +++ b/packages/nestjs-auth-local/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@concepta/nestjs-auth-jwt": "^6.0.0-alpha.0", "@concepta/nestjs-jwt": "^6.0.0-alpha.0", + "@concepta/nestjs-event": "^6.0.0-alpha.0", "@nestjs/testing": "^10.4.1", "@types/passport-local": "^1.0.38", "@types/supertest": "^2.0.16", diff --git a/packages/nestjs-auth-local/src/__fixtures__/app.module.fixture.ts b/packages/nestjs-auth-local/src/__fixtures__/app.module.fixture.ts index bf961c250..c79922957 100644 --- a/packages/nestjs-auth-local/src/__fixtures__/app.module.fixture.ts +++ b/packages/nestjs-auth-local/src/__fixtures__/app.module.fixture.ts @@ -7,11 +7,13 @@ import { AuthJwtModule } from '@concepta/nestjs-auth-jwt'; import { AuthLocalModule } from '../auth-local.module'; import { UserLookupServiceFixture } from './user/user-lookup.service.fixture'; import { UserModuleFixture } from './user/user.module.fixture'; +import { EventModule } from '@concepta/nestjs-event'; @Module({ imports: [ JwtModule.forRoot({}), AuthenticationModule.forRoot({}), + EventModule.forRoot({}), AuthJwtModule.forRootAsync({ inject: [UserLookupServiceFixture], useFactory: (userLookupService: UserLookupServiceFixture) => ({ diff --git a/packages/nestjs-auth-local/src/auth-local.constants.ts b/packages/nestjs-auth-local/src/auth-local.constants.ts index 60dc0df26..0ddc7c129 100644 --- a/packages/nestjs-auth-local/src/auth-local.constants.ts +++ b/packages/nestjs-auth-local/src/auth-local.constants.ts @@ -17,3 +17,5 @@ export const AUTH_LOCAL_MODULE_PASSWORD_VALIDATION_SERVICE_TOKEN = 'AUTH_LOCAL_MODULE_PASSWORD_VALIDATION_SERVICE_TOKEN'; export const AUTH_LOCAL_STRATEGY_NAME = 'local'; + +export const AUTH_LOCAL_AUTHENTICATION_TYPE = 'auth-local'; diff --git a/packages/nestjs-auth-local/src/auth-local.controller.spec.ts b/packages/nestjs-auth-local/src/auth-local.controller.spec.ts index 35f48082b..af08e5324 100644 --- a/packages/nestjs-auth-local/src/auth-local.controller.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.controller.spec.ts @@ -2,15 +2,21 @@ import { IssueTokenServiceInterface } from '@concepta/nestjs-authentication'; import { AuthenticatedUserInterface, AuthenticationResponseInterface, + AuthHistoryLoginInterface, } from '@concepta/nestjs-common'; import { randomUUID } from 'crypto'; import { mock } from 'jest-mock-extended'; import { AuthLocalController } from './auth-local.controller'; +import { EventDispatchService } from '@concepta/nestjs-event'; +import { AUTH_LOCAL_AUTHENTICATION_TYPE } from './auth-local.constants'; +import { AuthLocalAuthenticatedEventAsync } from './events/auth-local-authenticated.event'; describe(AuthLocalController, () => { const accessToken = 'accessToken'; const refreshToken = 'refreshToken'; let controller: AuthLocalController; + let eventDispatchService: EventDispatchService; + let spyOnDispatchService: jest.SpyInstance; const response: AuthenticationResponseInterface = { accessToken, refreshToken, @@ -24,7 +30,15 @@ describe(AuthLocalController, () => { }); }, }); - controller = new AuthLocalController(issueTokenService); + eventDispatchService = mock(); + spyOnDispatchService = jest + .spyOn(eventDispatchService, 'async') + .mockResolvedValue([]); + // eventDispatchService.async. + controller = new AuthLocalController( + issueTokenService, + eventDispatchService, + ); }); describe(AuthLocalController.prototype.login, () => { @@ -32,8 +46,20 @@ describe(AuthLocalController, () => { const user: AuthenticatedUserInterface = { id: randomUUID(), }; - const result = await controller.login(user); + const authLogin: AuthHistoryLoginInterface = { + ipAddress: '127.0.0.1', + deviceInfo: 'IOS', + }; + const result = await controller.login(user, authLogin); expect(result.accessToken).toBe(response.accessToken); + const authenticatedEventAsync = new AuthLocalAuthenticatedEventAsync({ + userInfo: { + userId: user.id, + authType: AUTH_LOCAL_AUTHENTICATION_TYPE, + ...authLogin, + }, + }); + expect(spyOnDispatchService).toBeCalledWith(authenticatedEventAsync); }); }); }); diff --git a/packages/nestjs-auth-local/src/auth-local.controller.ts b/packages/nestjs-auth-local/src/auth-local.controller.ts index bf227ce55..f6d851013 100644 --- a/packages/nestjs-auth-local/src/auth-local.controller.ts +++ b/packages/nestjs-auth-local/src/auth-local.controller.ts @@ -1,3 +1,17 @@ +import { + AuthenticationJwtResponseDto, + AuthPublic, + AuthUser, + IssueTokenServiceInterface, +} from '@concepta/nestjs-authentication'; +import { + AuthenticatedUserInterface, + AuthenticationResponseInterface, + AuthenticatedEventInterface, + AuthInfo, + AuthenticatedUserInfoInterface, +} from '@concepta/nestjs-common'; +import { EventDispatchService } from '@concepta/nestjs-event'; import { Controller, Inject, Post, UseGuards } from '@nestjs/common'; import { ApiBody, @@ -6,18 +20,12 @@ import { ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { - AuthenticatedUserInterface, - AuthenticationResponseInterface, -} from '@concepta/nestjs-common'; -import { - AuthUser, - IssueTokenServiceInterface, - AuthenticationJwtResponseDto, - AuthPublic, -} from '@concepta/nestjs-authentication'; -import { AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN } from './auth-local.constants'; -import { AuthLocalLoginDto } from './dto/auth-local-login.dto'; + AUTH_LOCAL_AUTHENTICATION_TYPE, + AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, +} from './auth-local.constants'; import { AuthLocalGuard } from './auth-local.guard'; +import { AuthLocalLoginDto } from './dto/auth-local-login.dto'; +import { AuthLocalAuthenticatedEventAsync } from './events/auth-local-authenticated.event'; /** * Auth Local controller @@ -30,6 +38,7 @@ export class AuthLocalController { constructor( @Inject(AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, + private readonly eventDispatchService: EventDispatchService, ) {} /** @@ -47,7 +56,33 @@ export class AuthLocalController { @Post() async login( @AuthUser() user: AuthenticatedUserInterface, + @AuthInfo() authInfo: AuthenticatedUserInfoInterface, ): Promise { - return this.issueTokenService.responsePayload(user.id); + const response = await this.issueTokenService.responsePayload(user.id); + + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_LOCAL_AUTHENTICATION_TYPE, + }, + }); + + return response; + } + + protected async dispatchAuthenticatedEvent( + payload?: AuthenticatedEventInterface, + ): Promise { + const authenticatedEventAsync = new AuthLocalAuthenticatedEventAsync( + payload, + ); + + const eventResult = await this.eventDispatchService.async( + authenticatedEventAsync, + ); + + return eventResult.every((it) => it === true); } } diff --git a/packages/nestjs-auth-local/src/auth-local.module.spec.ts b/packages/nestjs-auth-local/src/auth-local.module.spec.ts index 2a717735b..ad7f1df6e 100644 --- a/packages/nestjs-auth-local/src/auth-local.module.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.module.spec.ts @@ -34,6 +34,7 @@ import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.int import { UserLookupServiceFixture } from './__fixtures__/user/user-lookup.service.fixture'; import { UserModuleFixture } from './__fixtures__/user/user.module.fixture'; import { AuthLocalValidateUserService } from './services/auth-local-validate-user.service'; +import { EventModule } from '@concepta/nestjs-event'; describe(AuthLocalModule, () => { const jwtService = new JwtService(); @@ -217,6 +218,7 @@ function testModuleFactory( UserModuleFixture, AuthenticationModule.forRoot({}), JwtModule.forRoot({}), + EventModule.forRoot({}), ...extraImports, ], }; diff --git a/packages/nestjs-auth-local/src/events/auth-local-authenticated.event.ts b/packages/nestjs-auth-local/src/events/auth-local-authenticated.event.ts new file mode 100644 index 000000000..b7ac21e55 --- /dev/null +++ b/packages/nestjs-auth-local/src/events/auth-local-authenticated.event.ts @@ -0,0 +1,7 @@ +import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; +import { EventAsync } from '@concepta/nestjs-event'; + +export class AuthLocalAuthenticatedEventAsync extends EventAsync< + AuthenticatedEventInterface, + boolean +> {} diff --git a/packages/nestjs-auth-local/src/index.ts b/packages/nestjs-auth-local/src/index.ts index c938215d8..b3d94d66c 100644 --- a/packages/nestjs-auth-local/src/index.ts +++ b/packages/nestjs-auth-local/src/index.ts @@ -6,6 +6,7 @@ export { AuthLocalCredentialsInterface } from './interfaces/auth-local-credentia // DTOs export { AuthLocalLoginDto } from './dto/auth-local-login.dto'; +export { AuthLocalAuthenticatedEventAsync } from './events/auth-local-authenticated.event'; // module export { AuthLocalModule } from './auth-local.module'; diff --git a/packages/nestjs-common/src/decorators/auth-info.decorator.ts b/packages/nestjs-common/src/decorators/auth-info.decorator.ts new file mode 100644 index 000000000..04ae28f41 --- /dev/null +++ b/packages/nestjs-common/src/decorators/auth-info.decorator.ts @@ -0,0 +1,23 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { AuthenticatedUserInfoInterface } from '../domain/authentication/interfaces/authenticated-user-info.interface'; + +export const AuthInfo = createParamDecorator( + ( + data: keyof AuthenticatedUserInfoInterface | undefined, + ctx: ExecutionContext, + ): AuthenticatedUserInfoInterface | string | undefined => { + const request = ctx.switchToHttp().getRequest(); + const ipAddress = + request.connection?.ip || + request.headers['x-forwarded-for'] || + request.connection?.remoteAddress || + ''; + const deviceInfo = request.headers['user-agent'] || ''; + const userLoginInfo: AuthenticatedUserInfoInterface = { + ipAddress, + deviceInfo, + }; + + return data ? userLoginInfo?.[data] : userLoginInfo; + }, +); diff --git a/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-creatable.interface.ts b/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-creatable.interface.ts new file mode 100644 index 000000000..168f3b8cb --- /dev/null +++ b/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-creatable.interface.ts @@ -0,0 +1,4 @@ +import { AuthenticatedUserRequestInterface } from '../../authentication/interfaces/authenticated-info.interface'; + +export interface AuthHistoryCreatableInterface + extends AuthenticatedUserRequestInterface {} diff --git a/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-ownable.interface.ts b/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-ownable.interface.ts new file mode 100644 index 000000000..a30b508b9 --- /dev/null +++ b/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history-ownable.interface.ts @@ -0,0 +1,7 @@ +import { ReferenceId } from '../../../reference/interfaces/reference.types'; +import { AuthHistoryInterface } from './auth-history.interface'; + +export interface AuthHistoryOwnableInterface { + authHistoryId: ReferenceId; + authHistory?: AuthHistoryInterface; +} diff --git a/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history.interface.ts b/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history.interface.ts new file mode 100644 index 000000000..f430aa77e --- /dev/null +++ b/packages/nestjs-common/src/domain/auth-history/interfaces/auth-history.interface.ts @@ -0,0 +1,14 @@ +import { AuditInterface } from '../../../audit/interfaces/audit.interface'; +import { ReferenceIdInterface } from '../../../reference/interfaces/reference-id.interface'; +import { AuthenticatedUserRequestInterface } from '../../authentication/interfaces/authenticated-info.interface'; +import { UserOwnableInterface } from '../../user/interfaces/user-ownable.interface'; + +export interface AuthHistoryInterface + extends ReferenceIdInterface, + UserOwnableInterface, + AuditInterface, + Pick< + AuthenticatedUserRequestInterface, + 'authType' | 'ipAddress' | 'deviceInfo' + >, + Partial> {} diff --git a/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-event-payload.interface.ts b/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-event-payload.interface.ts new file mode 100644 index 000000000..525fb41f2 --- /dev/null +++ b/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-event-payload.interface.ts @@ -0,0 +1,7 @@ +import { ReferenceQueryOptionsInterface } from '../../../reference/interfaces/reference-query-options.interface'; +import { AuthenticatedUserRequestInterface } from './authenticated-info.interface'; + +export interface AuthenticatedEventInterface { + userInfo: AuthenticatedUserRequestInterface; + queryOptions?: ReferenceQueryOptionsInterface; +} diff --git a/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-info.interface.ts b/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-info.interface.ts new file mode 100644 index 000000000..360019439 --- /dev/null +++ b/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-info.interface.ts @@ -0,0 +1,8 @@ +import { UserOwnableInterface } from '../../user/interfaces/user-ownable.interface'; + +export interface AuthenticatedUserRequestInterface + extends Pick { + ipAddress: string; + authType: string; + deviceInfo?: string; +} diff --git a/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-user-info.interface.ts b/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-user-info.interface.ts new file mode 100644 index 000000000..dc2ae7c33 --- /dev/null +++ b/packages/nestjs-common/src/domain/authentication/interfaces/authenticated-user-info.interface.ts @@ -0,0 +1,4 @@ +import { AuthenticatedUserRequestInterface } from './authenticated-info.interface'; + +export interface AuthenticatedUserInfoInterface + extends Pick {} diff --git a/packages/nestjs-common/src/domain/index.ts b/packages/nestjs-common/src/domain/index.ts index 9fae9502d..a11f5eda0 100644 --- a/packages/nestjs-common/src/domain/index.ts +++ b/packages/nestjs-common/src/domain/index.ts @@ -2,11 +2,14 @@ export { EmailSendOptionsInterface } from './email/interfaces/email-send-options export { EmailSendInterface } from './email/interfaces/email-send.interface'; export { AuthenticatedUserInterface } from './authentication/interfaces/authenticated-user.interface'; +export { AuthenticatedUserInfoInterface } from './authentication/interfaces/authenticated-user-info.interface'; export { AuthenticationAccessInterface } from './authentication/interfaces/authentication-access.interface'; export { AuthenticationCodeInterface } from './authentication/interfaces/authentication-code.interface'; export { AuthenticationLoginInterface } from './authentication/interfaces/authentication-login.interface'; export { AuthenticationRefreshInterface } from './authentication/interfaces/authentication-refresh.interface'; export { AuthenticationResponseInterface } from './authentication/interfaces/authentication-response.interface'; +export { AuthenticatedEventInterface } from './authentication/interfaces/authenticated-event-payload.interface'; +export { AuthenticatedUserRequestInterface } from './authentication/interfaces/authenticated-info.interface'; export { AuthorizationPayloadInterface } from './authorization/interfaces/authorization-payload.interface'; @@ -24,6 +27,11 @@ export { UserOwnableInterface } from './user/interfaces/user-ownable.interface'; export { UserUpdatableInterface } from './user/interfaces/user-updatable.interface'; export { UserInterface } from './user/interfaces/user.interface'; +export { AuthHistoryCreatableInterface } from './auth-history/interfaces/auth-history-creatable.interface'; +export { AuthenticatedUserInfoInterface as AuthHistoryLoginInterface } from './authentication/interfaces/authenticated-user-info.interface'; +export { AuthHistoryOwnableInterface } from './auth-history/interfaces/auth-history-ownable.interface'; +export { AuthHistoryInterface } from './auth-history/interfaces/auth-history.interface'; + export { FederatedCreatableInterface } from './federated/interfaces/federated-creatable.interface'; export { FederatedUpdatableInterface } from './federated/interfaces/federated-updatable.interface'; export { FederatedInterface } from './federated/interfaces/federated.interface'; diff --git a/packages/nestjs-common/src/index.ts b/packages/nestjs-common/src/index.ts index fbe8b97a4..97ccaed96 100644 --- a/packages/nestjs-common/src/index.ts +++ b/packages/nestjs-common/src/index.ts @@ -5,6 +5,7 @@ export { ReferenceIdDto } from './reference/dto/reference-id.dto'; // Decorators export { AuthUser } from './decorators/auth-user.decorator'; +export { AuthInfo } from './decorators/auth-info.decorator'; // Module utilities export { createSettingsProvider } from './modules/utils/create-settings-provider'; diff --git a/packages/nestjs-crud/src/services/typeorm-crud.service.ts b/packages/nestjs-crud/src/services/typeorm-crud.service.ts index e5ff7ca65..8e75742e1 100644 --- a/packages/nestjs-crud/src/services/typeorm-crud.service.ts +++ b/packages/nestjs-crud/src/services/typeorm-crud.service.ts @@ -92,6 +92,7 @@ export class TypeOrmCrudService< ): ReturnType['createOne']> { // apply options this.crudQueryHelper.modifyRequest(req, queryOptions); + // return parent result try { return super.createOne(req, dto); diff --git a/packages/nestjs-samples/src/03-authentication/app.module.ts b/packages/nestjs-samples/src/03-authentication/app.module.ts index 8682f721a..3f18c42ed 100644 --- a/packages/nestjs-samples/src/03-authentication/app.module.ts +++ b/packages/nestjs-samples/src/03-authentication/app.module.ts @@ -11,6 +11,7 @@ import { CrudModule } from '@concepta/nestjs-crud'; import { CustomUserController } from './user/user.controller'; import { UserEntity } from './user/user.entity'; import { createUserRepository } from './user/create-user-repository'; +import { EventModule } from '@concepta/nestjs-event'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { createUserRepository } from './user/create-user-repository'; JwtModule.forRoot({}), PasswordModule.forRoot({}), CrudModule.forRoot({}), + EventModule.forRoot({}), UserModule.forRoot({ entities: { user: { diff --git a/tsconfig.json b/tsconfig.json index 9d0a5435f..479693164 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -109,6 +109,9 @@ }, { "path": "packages/nestjs-report" + }, + { + "path": "packages/nestjs-auth-history" } ] } diff --git a/yarn.lock b/yarn.lock index 0bb4b564f..2e5cacb6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -870,6 +870,38 @@ __metadata: languageName: unknown linkType: soft +"@concepta/nestjs-auth-history@workspace:packages/nestjs-auth-history": + version: 0.0.0-use.local + resolution: "@concepta/nestjs-auth-history@workspace:packages/nestjs-auth-history" + dependencies: + "@concepta/nestjs-access-control": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-auth-jwt": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-authentication": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-common": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-crud": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-event": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-exception": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-jwt": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-password": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-typeorm-ext": "npm:^6.0.0-alpha.0" + "@concepta/typeorm-common": "npm:^6.0.0-alpha.0" + "@concepta/typeorm-seeding": "npm:^4.0.0" + "@faker-js/faker": "npm:^8.4.1" + "@nestjs/common": "npm:^10.4.1" + "@nestjs/config": "npm:^3.2.3" + "@nestjs/core": "npm:^10.4.1" + "@nestjs/swagger": "npm:^7.4.0" + "@nestjs/testing": "npm:^10.4.1" + "@nestjs/typeorm": "npm:^10.0.2" + accesscontrol: "npm:^2.2.1" + supertest: "npm:^6.3.4" + peerDependencies: + class-transformer: "*" + class-validator: "*" + typeorm: ^0.3.0 + languageName: unknown + linkType: soft + "@concepta/nestjs-auth-jwt@npm:^6.0.0-alpha.0, @concepta/nestjs-auth-jwt@workspace:packages/nestjs-auth-jwt": version: 0.0.0-use.local resolution: "@concepta/nestjs-auth-jwt@workspace:packages/nestjs-auth-jwt" From fbe1ed8b878cd6523ee68424ff210bd548b88f7f Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Thu, 16 Jan 2025 18:32:08 -0300 Subject: [PATCH 2/7] chore: linting --- packages/nestjs-auth-history/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nestjs-auth-history/README.md b/packages/nestjs-auth-history/README.md index ffc6a7abc..7b94240d8 100644 --- a/packages/nestjs-auth-history/README.md +++ b/packages/nestjs-auth-history/README.md @@ -1,6 +1,9 @@ # Rockets NestJS Auth History -A module for tracking authentication history and events, providing services for creating, reading, updating and deleting auth history records. Includes event handling for authenticated requests, repository management, and access control. +A module for tracking authentication history and events, providing services +for creating, reading, updating and deleting auth history records. Includes +event handling for authenticated requests, repository management, and access +control. ## Project From 972fbe43f52522323a6077b70aef57c2de17bb1f Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Sat, 18 Jan 2025 13:11:14 -0300 Subject: [PATCH 3/7] chore: packages updates --- packages/nestjs-auth-apple/package.json | 2 +- packages/nestjs-auth-local/package.json | 2 +- yarn.lock | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nestjs-auth-apple/package.json b/packages/nestjs-auth-apple/package.json index 938e7c94c..83805880f 100644 --- a/packages/nestjs-auth-apple/package.json +++ b/packages/nestjs-auth-apple/package.json @@ -14,8 +14,8 @@ "dependencies": { "@concepta/nestjs-authentication": "^6.0.0-alpha.0", "@concepta/nestjs-common": "^6.0.0-alpha.0", - "@concepta/nestjs-exception": "^6.0.0-alpha.0", "@concepta/nestjs-event": "^6.0.0-alpha.0", + "@concepta/nestjs-exception": "^6.0.0-alpha.0", "@concepta/nestjs-federated": "^6.0.0-alpha.0", "@concepta/nestjs-jwt": "^6.0.0-alpha.0", "@nestjs/common": "^10.4.1", diff --git a/packages/nestjs-auth-local/package.json b/packages/nestjs-auth-local/package.json index 430dfbdfa..f00d33669 100644 --- a/packages/nestjs-auth-local/package.json +++ b/packages/nestjs-auth-local/package.json @@ -25,8 +25,8 @@ }, "devDependencies": { "@concepta/nestjs-auth-jwt": "^6.0.0-alpha.0", - "@concepta/nestjs-jwt": "^6.0.0-alpha.0", "@concepta/nestjs-event": "^6.0.0-alpha.0", + "@concepta/nestjs-jwt": "^6.0.0-alpha.0", "@nestjs/testing": "^10.4.1", "@types/passport-local": "^1.0.38", "@types/supertest": "^2.0.16", diff --git a/yarn.lock b/yarn.lock index 6c4ca7799..2b221256d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -787,6 +787,7 @@ __metadata: "@concepta/nestjs-authentication": "npm:^6.0.0-alpha.0" "@concepta/nestjs-common": "npm:^6.0.0-alpha.0" "@concepta/nestjs-crud": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-event": "npm:^6.0.0-alpha.0" "@concepta/nestjs-exception": "npm:^6.0.0-alpha.0" "@concepta/nestjs-federated": "npm:^6.0.0-alpha.0" "@concepta/nestjs-jwt": "npm:^6.0.0-alpha.0" @@ -884,6 +885,7 @@ __metadata: "@concepta/nestjs-jwt": "npm:^6.0.0-alpha.0" "@concepta/nestjs-password": "npm:^6.0.0-alpha.0" "@concepta/nestjs-typeorm-ext": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-user": "npm:^6.0.0-alpha.0" "@concepta/typeorm-common": "npm:^6.0.0-alpha.0" "@concepta/typeorm-seeding": "npm:^4.0.0" "@faker-js/faker": "npm:^8.4.1" @@ -896,6 +898,7 @@ __metadata: accesscontrol: "npm:^2.2.1" supertest: "npm:^6.3.4" peerDependencies: + "@concepta/nestjs-auth-local": ^6.0.0-alpha.0 class-transformer: "*" class-validator: "*" typeorm: ^0.3.0 @@ -930,6 +933,7 @@ __metadata: "@concepta/nestjs-auth-jwt": "npm:^6.0.0-alpha.0" "@concepta/nestjs-authentication": "npm:^6.0.0-alpha.0" "@concepta/nestjs-common": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-event": "npm:^6.0.0-alpha.0" "@concepta/nestjs-exception": "npm:^6.0.0-alpha.0" "@concepta/nestjs-jwt": "npm:^6.0.0-alpha.0" "@concepta/nestjs-password": "npm:^6.0.0-alpha.0" From e982f8cc54e3deeebb460fa54e4167001f557e82 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Wed, 22 Jan 2025 11:31:52 -0300 Subject: [PATCH 4/7] chore: add events and tests --- .../src/auth-apple.controller.ts | 47 ++++++++----- packages/nestjs-auth-github/package.json | 1 + .../src/auth-github.constants.ts | 2 + .../src/auth-github.controller.ts | 47 ++++++++++++- .../events/auth-github-authenticated.event.ts | 7 ++ packages/nestjs-auth-google/package.json | 1 + .../src/auth-google.constants.ts | 2 + .../src/auth-google.controller.ts | 47 ++++++++++++- .../events/auth-google-authenticated.event.ts | 7 ++ .../src/__fixtures__/app.module.fixture.ts | 17 ++++- .../src/__fixtures__/constants.ts | 21 ++++++ .../services/user-lookup.service.fixture.ts | 19 +++++ .../services/validate-user.service.fixture.ts | 27 +++++++ .../user-lookup.custom.service.ts | 30 -------- .../src/auth-history.controller.e2e-spec.ts | 70 +++++++++++++++++-- .../src/auth-history.module-definition.ts | 4 ++ .../src/listeners/authenticated-listener.ts | 1 + .../src/auth-local.controller.ts | 40 ++++++----- .../src/decorators/auth-info.decorator.ts | 2 + 19 files changed, 317 insertions(+), 75 deletions(-) create mode 100644 packages/nestjs-auth-github/src/events/auth-github-authenticated.event.ts create mode 100644 packages/nestjs-auth-google/src/events/auth-google-authenticated.event.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/constants.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/services/user-lookup.service.fixture.ts create mode 100644 packages/nestjs-auth-history/src/__fixtures__/services/validate-user.service.fixture.ts delete mode 100644 packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts diff --git a/packages/nestjs-auth-apple/src/auth-apple.controller.ts b/packages/nestjs-auth-apple/src/auth-apple.controller.ts index b4cbc2062..c13558df4 100644 --- a/packages/nestjs-auth-apple/src/auth-apple.controller.ts +++ b/packages/nestjs-auth-apple/src/auth-apple.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Inject, Get, UseGuards, Post } from '@nestjs/common'; +import { + Controller, + Inject, + Get, + UseGuards, + Post, + Optional, +} from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AuthenticatedEventInterface, @@ -44,7 +51,9 @@ export class AuthAppleController { constructor( @Inject(AUTH_APPLE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, - private readonly eventDispatchService: EventDispatchService, + @Optional() + @Inject(EventDispatchService) + private readonly eventDispatchService?: EventDispatchService, ) {} /** @@ -70,14 +79,15 @@ export class AuthAppleController { ): Promise { const response = this.issueTokenService.responsePayload(user.id); - await this.dispatchAuthenticatedEvent({ - userInfo: { - userId: user.id, - ipAddress: authInfo?.ipAddress || '', - deviceInfo: authInfo?.deviceInfo || '', - authType: AUTH_APPLE_AUTHENTICATION_TYPE, - }, - }); + if (this.eventDispatchService) + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_APPLE_AUTHENTICATION_TYPE, + }, + }); return response; } @@ -85,14 +95,17 @@ export class AuthAppleController { protected async dispatchAuthenticatedEvent( payload?: AuthenticatedEventInterface, ): Promise { - const authenticatedEventAsync = new AuthAppleAuthenticatedEventAsync( - payload, - ); + if (this.eventDispatchService) { + const authenticatedEventAsync = new AuthAppleAuthenticatedEventAsync( + payload, + ); - const eventResult = await this.eventDispatchService.async( - authenticatedEventAsync, - ); + const eventResult = await this.eventDispatchService.async( + authenticatedEventAsync, + ); - return eventResult.every((it) => it === true); + return eventResult.every((it) => it === true); + } + return true; } } diff --git a/packages/nestjs-auth-github/package.json b/packages/nestjs-auth-github/package.json index d859a45a6..233b86738 100644 --- a/packages/nestjs-auth-github/package.json +++ b/packages/nestjs-auth-github/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@concepta/nestjs-auth-jwt": "^6.0.0-alpha.0", "@concepta/nestjs-crud": "^6.0.0-alpha.0", + "@concepta/nestjs-event": "^6.0.0-alpha.0", "@concepta/nestjs-jwt": "^6.0.0-alpha.0", "@concepta/nestjs-password": "^6.0.0-alpha.0", "@concepta/nestjs-typeorm-ext": "^6.0.0-alpha.0", diff --git a/packages/nestjs-auth-github/src/auth-github.constants.ts b/packages/nestjs-auth-github/src/auth-github.constants.ts index 7f6aa60b8..c38959a8e 100644 --- a/packages/nestjs-auth-github/src/auth-github.constants.ts +++ b/packages/nestjs-auth-github/src/auth-github.constants.ts @@ -8,3 +8,5 @@ export const AUTH_GITHUB_MODULE_DEFAULT_SETTINGS_TOKEN = 'AUTH_GITHUB_MODULE_DEFAULT_SETTINGS_TOKEN'; export const AUTH_GITHUB_STRATEGY_NAME = 'github'; + +export const AUTH_GITHUB_AUTHENTICATION_TYPE = 'auth-github'; diff --git a/packages/nestjs-auth-github/src/auth-github.controller.ts b/packages/nestjs-auth-github/src/auth-github.controller.ts index b1f0acedb..48c49daf6 100644 --- a/packages/nestjs-auth-github/src/auth-github.controller.ts +++ b/packages/nestjs-auth-github/src/auth-github.controller.ts @@ -1,8 +1,11 @@ -import { Controller, Inject, Get, UseGuards } from '@nestjs/common'; +import { Controller, Inject, Get, UseGuards, Optional } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { + AuthenticatedEventInterface, + AuthenticatedUserInfoInterface, AuthenticatedUserInterface, AuthenticationResponseInterface, + AuthInfo, } from '@concepta/nestjs-common'; import { AuthUser, @@ -10,8 +13,13 @@ import { AuthenticationJwtResponseDto, AuthPublic, } from '@concepta/nestjs-authentication'; -import { AUTH_GITHUB_ISSUE_TOKEN_SERVICE_TOKEN } from './auth-github.constants'; +import { + AUTH_GITHUB_AUTHENTICATION_TYPE, + AUTH_GITHUB_ISSUE_TOKEN_SERVICE_TOKEN, +} from './auth-github.constants'; import { AuthGithubGuard } from './auth-github.guard'; +import { EventDispatchService } from '@concepta/nestjs-event'; +import { AuthGithubAuthenticatedEventAsync } from './events/auth-github-authenticated.event'; // TODO: improve documentation /** @@ -37,6 +45,9 @@ export class AuthGithubController { constructor( @Inject(AUTH_GITHUB_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, + @Optional() + @Inject(EventDispatchService) + private readonly eventDispatchService: EventDispatchService, ) {} /** @@ -59,7 +70,37 @@ export class AuthGithubController { @Get('callback') async get( @AuthUser() user: AuthenticatedUserInterface, + @AuthInfo() authInfo: AuthenticatedUserInfoInterface, ): Promise { - return this.issueTokenService.responsePayload(user.id); + const response = this.issueTokenService.responsePayload(user.id); + + if (this.eventDispatchService) + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_GITHUB_AUTHENTICATION_TYPE, + }, + }); + + return response; + } + + protected async dispatchAuthenticatedEvent( + payload?: AuthenticatedEventInterface, + ): Promise { + if (this.eventDispatchService) { + const authenticatedEventAsync = new AuthGithubAuthenticatedEventAsync( + payload, + ); + + const eventResult = await this.eventDispatchService.async( + authenticatedEventAsync, + ); + + return eventResult.every((it) => it === true); + } + return true; } } diff --git a/packages/nestjs-auth-github/src/events/auth-github-authenticated.event.ts b/packages/nestjs-auth-github/src/events/auth-github-authenticated.event.ts new file mode 100644 index 000000000..c7137696d --- /dev/null +++ b/packages/nestjs-auth-github/src/events/auth-github-authenticated.event.ts @@ -0,0 +1,7 @@ +import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; +import { EventAsync } from '@concepta/nestjs-event'; + +export class AuthGithubAuthenticatedEventAsync extends EventAsync< + AuthenticatedEventInterface, + boolean +> {} diff --git a/packages/nestjs-auth-google/package.json b/packages/nestjs-auth-google/package.json index 2cd1df3c8..af31006f5 100644 --- a/packages/nestjs-auth-google/package.json +++ b/packages/nestjs-auth-google/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@concepta/nestjs-auth-jwt": "^6.0.0-alpha.0", "@concepta/nestjs-crud": "^6.0.0-alpha.0", + "@concepta/nestjs-event": "^6.0.0-alpha.0", "@concepta/nestjs-jwt": "^6.0.0-alpha.0", "@concepta/nestjs-password": "^6.0.0-alpha.0", "@concepta/nestjs-typeorm-ext": "^6.0.0-alpha.0", diff --git a/packages/nestjs-auth-google/src/auth-google.constants.ts b/packages/nestjs-auth-google/src/auth-google.constants.ts index e694bf420..4224bfcf2 100644 --- a/packages/nestjs-auth-google/src/auth-google.constants.ts +++ b/packages/nestjs-auth-google/src/auth-google.constants.ts @@ -8,3 +8,5 @@ export const AUTH_GOOGLE_MODULE_DEFAULT_SETTINGS_TOKEN = 'AUTH_GOOGLE_MODULE_DEFAULT_SETTINGS_TOKEN'; export const AUTH_GOOGLE_STRATEGY_NAME = 'google'; + +export const AUTH_GOOGLE_AUTHENTICATION_TYPE = 'auth-google'; diff --git a/packages/nestjs-auth-google/src/auth-google.controller.ts b/packages/nestjs-auth-google/src/auth-google.controller.ts index 3b1505d03..4b2ae30b1 100644 --- a/packages/nestjs-auth-google/src/auth-google.controller.ts +++ b/packages/nestjs-auth-google/src/auth-google.controller.ts @@ -1,8 +1,11 @@ -import { Controller, Inject, Get, UseGuards } from '@nestjs/common'; +import { Controller, Inject, Get, UseGuards, Optional } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { + AuthenticatedEventInterface, + AuthenticatedUserInfoInterface, AuthenticatedUserInterface, AuthenticationResponseInterface, + AuthInfo, } from '@concepta/nestjs-common'; import { AuthUser, @@ -10,8 +13,13 @@ import { AuthenticationJwtResponseDto, AuthPublic, } from '@concepta/nestjs-authentication'; -import { AUTH_GOOGLE_ISSUE_TOKEN_SERVICE_TOKEN } from './auth-google.constants'; +import { + AUTH_GOOGLE_AUTHENTICATION_TYPE, + AUTH_GOOGLE_ISSUE_TOKEN_SERVICE_TOKEN, +} from './auth-google.constants'; import { AuthGoogleGuard } from './auth-google.guard'; +import { EventDispatchService } from '@concepta/nestjs-event'; +import { AuthGoogleAuthenticatedEventAsync } from './events/auth-GOOGLE-authenticated.event'; /** * Google controller @@ -36,6 +44,9 @@ export class AuthGoogleController { constructor( @Inject(AUTH_GOOGLE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, + @Optional() + @Inject(EventDispatchService) + private readonly eventDispatchService?: EventDispatchService, ) {} /** @@ -57,7 +68,37 @@ export class AuthGoogleController { @Get('callback') async get( @AuthUser() user: AuthenticatedUserInterface, + @AuthInfo() authInfo: AuthenticatedUserInfoInterface, ): Promise { - return this.issueTokenService.responsePayload(user.id); + const response = this.issueTokenService.responsePayload(user.id); + + if (this.eventDispatchService) + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_GOOGLE_AUTHENTICATION_TYPE, + }, + }); + + return response; + } + + protected async dispatchAuthenticatedEvent( + payload?: AuthenticatedEventInterface, + ): Promise { + if (this.eventDispatchService) { + const authenticatedEventAsync = new AuthGoogleAuthenticatedEventAsync( + payload, + ); + + const eventResult = await this.eventDispatchService.async( + authenticatedEventAsync, + ); + + return eventResult.every((it) => it === true); + } + return true; } } diff --git a/packages/nestjs-auth-google/src/events/auth-google-authenticated.event.ts b/packages/nestjs-auth-google/src/events/auth-google-authenticated.event.ts new file mode 100644 index 000000000..b043a6d0a --- /dev/null +++ b/packages/nestjs-auth-google/src/events/auth-google-authenticated.event.ts @@ -0,0 +1,7 @@ +import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; +import { EventAsync } from '@concepta/nestjs-event'; + +export class AuthGoogleAuthenticatedEventAsync extends EventAsync< + AuthenticatedEventInterface, + boolean +> {} diff --git a/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts index 0d8e713a0..10b1be122 100644 --- a/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts +++ b/packages/nestjs-auth-history/src/__fixtures__/app.module.fixture.ts @@ -1,7 +1,10 @@ import { AccessControlModule } from '@concepta/nestjs-access-control'; +import { + AuthLocalAuthenticatedEventAsync, + AuthLocalModule, +} from '@concepta/nestjs-auth-local'; import { CrudModule } from '@concepta/nestjs-crud'; import { EventModule } from '@concepta/nestjs-event'; -import { AuthLocalAuthenticatedEventAsync } from '@concepta/nestjs-auth-local'; import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; import { Module } from '@nestjs/common'; import { AccessControl } from 'accesscontrol'; @@ -9,9 +12,13 @@ import { AccessControl } from 'accesscontrol'; import { AuthHistoryModule } from '../auth-history.module'; import { AuthHistoryResource } from '../auth-history.types'; +import { AuthenticationModule } from '@concepta/nestjs-authentication'; +import { JwtModule } from '@concepta/nestjs-jwt'; import { AuthHistoryAccessQueryService } from '../services/auth-history-query.service'; import { AuthHistoryEntityFixture } from './entities/auth-history.entity.fixture'; import { ormConfig } from './ormconfig.fixture'; +import { UserLookupServiceFixture } from './services/user-lookup.service.fixture'; +import { ValidateUserServiceFixture } from './services/validate-user.service.fixture'; const rules = new AccessControl(); rules @@ -27,10 +34,18 @@ rules TypeOrmExtModule.forRoot(ormConfig), CrudModule.forRoot({}), EventModule.forRoot({}), + JwtModule.forRoot({}), + AuthenticationModule.forRoot({}), AccessControlModule.forRoot({ settings: { rules }, queryServices: [AuthHistoryAccessQueryService], }), + AuthLocalModule.forRootAsync({ + useFactory: () => ({ + userLookupService: new UserLookupServiceFixture(), + validateUserService: new ValidateUserServiceFixture(), + }), + }), AuthHistoryModule.forRoot({ settings: { authenticatedEvents: [AuthLocalAuthenticatedEventAsync], diff --git a/packages/nestjs-auth-history/src/__fixtures__/constants.ts b/packages/nestjs-auth-history/src/__fixtures__/constants.ts new file mode 100644 index 000000000..ae061f821 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/constants.ts @@ -0,0 +1,21 @@ +import { AuthLocalCredentialsInterface } from '@concepta/nestjs-auth-local'; +import { randomUUID } from 'crypto'; + +export const USER_ID = randomUUID(); +export const LOGIN_SUCCESS = { + username: 'random_username', + password: 'random_password', +}; + +export const LOGIN_FAIL = { + username: 'wrong_username', + password: 'wrong_password', +}; + +export const USER_SUCCESS: AuthLocalCredentialsInterface = { + id: USER_ID, + active: true, + passwordHash: LOGIN_SUCCESS.password, + passwordSalt: LOGIN_SUCCESS.password, + username: LOGIN_SUCCESS.username, +}; diff --git a/packages/nestjs-auth-history/src/__fixtures__/services/user-lookup.service.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/services/user-lookup.service.fixture.ts new file mode 100644 index 000000000..2d5ace566 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/services/user-lookup.service.fixture.ts @@ -0,0 +1,19 @@ +import { + AuthLocalCredentialsInterface, + AuthLocalUserLookupServiceInterface, +} from '@concepta/nestjs-auth-local'; +import { ReferenceUsername } from '@concepta/nestjs-common'; +import { Injectable } from '@nestjs/common'; +import { USER_SUCCESS } from '../constants'; + +@Injectable() +export class UserLookupServiceFixture + implements AuthLocalUserLookupServiceInterface +{ + async byUsername( + username: ReferenceUsername, + ): Promise { + if (USER_SUCCESS.username === username) return USER_SUCCESS; + return null; + } +} diff --git a/packages/nestjs-auth-history/src/__fixtures__/services/validate-user.service.fixture.ts b/packages/nestjs-auth-history/src/__fixtures__/services/validate-user.service.fixture.ts new file mode 100644 index 000000000..242e57b61 --- /dev/null +++ b/packages/nestjs-auth-history/src/__fixtures__/services/validate-user.service.fixture.ts @@ -0,0 +1,27 @@ +import { + AuthLocalUsernameNotFoundException, + AuthLocalValidateUserInterface, + AuthLocalValidateUserServiceInterface, +} from '@concepta/nestjs-auth-local'; +import { ValidateUserService } from '@concepta/nestjs-authentication'; +import { ReferenceIdInterface } from '@concepta/nestjs-common'; +import { Injectable } from '@nestjs/common'; +import { USER_SUCCESS } from '../constants'; + +@Injectable() +export class ValidateUserServiceFixture + extends ValidateUserService<[AuthLocalValidateUserInterface]> + implements AuthLocalValidateUserServiceInterface +{ + constructor() { + super(); + } + + async validateUser( + dto: AuthLocalValidateUserInterface, + ): Promise { + if (USER_SUCCESS.username === dto.username) return USER_SUCCESS; + + throw new AuthLocalUsernameNotFoundException(dto.username); + } +} diff --git a/packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts b/packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts deleted file mode 100644 index 674db556b..000000000 --- a/packages/nestjs-auth-history/src/__fixtures__/user-lookup.custom.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { UserInterface } from '@concepta/nestjs-common'; -import { - ReferenceEmail, - ReferenceId, - ReferenceSubject, - ReferenceUsername, -} from '@concepta/nestjs-common'; -import { AuthJwtUserLookupServiceInterface } from '@concepta/nestjs-auth-jwt/dist/interfaces/auth-jwt-user-lookup-service.interface'; - -@Injectable() -export class UserLookupCustomService - implements AuthJwtUserLookupServiceInterface -{ - async byId(id: ReferenceId): Promise { - throw new Error(`Method not implemented, cant get ${id}.`); - } - - async byEmail(email: ReferenceEmail): Promise { - throw new Error(`Method not implemented, cant get ${email}.`); - } - - async bySubject(subject: ReferenceSubject): Promise { - throw new Error(`Method not implemented, cant get ${subject}.`); - } - - async byUsername(username: ReferenceUsername): Promise { - throw new Error(`Method not implemented, cant get ${username}.`); - } -} diff --git a/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts b/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts index d8ed3ecfb..617098a04 100644 --- a/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts +++ b/packages/nestjs-auth-history/src/auth-history.controller.e2e-spec.ts @@ -17,6 +17,7 @@ import { AuthHistoryFactory } from './auth-history.factory'; import { UserInterface } from '@concepta/nestjs-common'; import { UserFactory } from '@concepta/nestjs-user/src/user.factory'; import { AppModuleFixture } from './__fixtures__/app.module.fixture'; +import { LOGIN_FAIL, LOGIN_SUCCESS, USER_ID } from './__fixtures__/constants'; import { AuthHistoryEntityFixture } from './__fixtures__/entities/auth-history.entity.fixture'; import { UserEntityFixture } from './__fixtures__/entities/user-entity.fixture'; @@ -35,10 +36,8 @@ describe('AuthHistoryController (e2e)', () => { app = moduleFixture.createNestApplication(); await app.init(); - // authJwtGuard = app.get(AuthJwtGuard); - // jest.spyOn(authJwtGuard, 'canActivate').mockResolvedValue(true); - accessControlGuard = app.get(AccessControlGuard); + jest.spyOn(accessControlGuard, 'canActivate').mockResolvedValue(true); accessControlFilter = app.get(AccessControlFilter); @@ -59,7 +58,9 @@ describe('AuthHistoryController (e2e)', () => { seedingSource, }); - user = await userFactory.create(); + user = await userFactory.create({ + id: USER_ID, + }); const authHistoryFactory = new AuthHistoryFactory({ entity: AuthHistoryEntityFixture, @@ -119,5 +120,66 @@ describe('AuthHistoryController (e2e)', () => { .delete(`/auth-history/${response.body[0].id}`) .expect(200); }); + + describe('events', () => { + it('should create auth history on login', async () => { + // attempt login it should trigger the event + await supertest(app.getHttpServer()) + .post('/auth/login') + .send(LOGIN_SUCCESS) + .expect(201); + + // verify auth history was created + const response = await supertest(app.getHttpServer()) + .get('/auth-history?filter=authType||$eq||auth-local') + .expect(200); + + expect(response.body[0]).toMatchObject({ + authType: 'auth-local', + userId: expect.any(String), + ipAddress: expect.any(String), + deviceInfo: expect.any(String), + }); + }); + + it('should login trigger event and validate user agent', async () => { + const userAgent = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1'; + // attempt login with wrong credentials + await supertest(app.getHttpServer()) + .post('/auth/login') + .set('User-Agent', userAgent) + .send(LOGIN_SUCCESS) + .expect(201); + + const response = await supertest(app.getHttpServer()) + .get('/auth-history?filter=authType||$eq||auth-local') + .expect(200); + + expect(response.body[0]).toMatchObject({ + authType: 'auth-local', + userId: expect.any(String), + ipAddress: expect.any(String), + deviceInfo: userAgent, + }); + }); + + it('should fail login should not trigger event', async () => { + const userAgent = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1'; + // attempt login with wrong credentials + await supertest(app.getHttpServer()) + .post('/auth/login') + .set('User-Agent', userAgent) + .send(LOGIN_FAIL) + .expect(500); + + const response = await supertest(app.getHttpServer()) + .get('/auth-history?filter=authType||$eq||auth-local') + .expect(200); + + expect(response.body.length).toBe(0); + }); + }); }); }); diff --git a/packages/nestjs-auth-history/src/auth-history.module-definition.ts b/packages/nestjs-auth-history/src/auth-history.module-definition.ts index bc05839e1..b46ea7708 100644 --- a/packages/nestjs-auth-history/src/auth-history.module-definition.ts +++ b/packages/nestjs-auth-history/src/auth-history.module-definition.ts @@ -19,6 +19,8 @@ import { AuthHistoryCrudService } from './services/auth-history-crud.service'; import { authHistoryDefaultConfig } from './config/auth-history-default.config'; import { AuthHistoryAccessQueryService } from './services/auth-history-query.service'; +import { AuthenticatedListener } from './listeners/authenticated-listener'; +import { AuthHistoryMutateService } from './services/auth-history-mutate.service'; const RAW_OPTIONS_TOKEN = Symbol('__AUTH_HISTORY_MODULE_RAW_OPTIONS_TOKEN__'); @@ -83,6 +85,8 @@ export function createAuthHistoryProviders(options: { return [ ...(options.providers ?? []), AuthHistoryCrudService, + AuthenticatedListener, + AuthHistoryMutateService, createAuthHistorySettingsProvider(options.overrides), createAuthHistoryAccessQueryServiceProvider(options.overrides), ]; diff --git a/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts b/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts index 1173476a3..fba6d54fb 100644 --- a/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts +++ b/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts @@ -59,6 +59,7 @@ export class AuthenticatedListener ); } catch (err) { Logger.error(err); + return false; } return true; } diff --git a/packages/nestjs-auth-local/src/auth-local.controller.ts b/packages/nestjs-auth-local/src/auth-local.controller.ts index f6d851013..624f90013 100644 --- a/packages/nestjs-auth-local/src/auth-local.controller.ts +++ b/packages/nestjs-auth-local/src/auth-local.controller.ts @@ -12,7 +12,7 @@ import { AuthenticatedUserInfoInterface, } from '@concepta/nestjs-common'; import { EventDispatchService } from '@concepta/nestjs-event'; -import { Controller, Inject, Post, UseGuards } from '@nestjs/common'; +import { Controller, Inject, Optional, Post, UseGuards } from '@nestjs/common'; import { ApiBody, ApiOkResponse, @@ -38,7 +38,9 @@ export class AuthLocalController { constructor( @Inject(AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, - private readonly eventDispatchService: EventDispatchService, + @Optional() + @Inject(EventDispatchService) + private readonly eventDispatchService?: EventDispatchService, ) {} /** @@ -60,14 +62,15 @@ export class AuthLocalController { ): Promise { const response = await this.issueTokenService.responsePayload(user.id); - await this.dispatchAuthenticatedEvent({ - userInfo: { - userId: user.id, - ipAddress: authInfo?.ipAddress || '', - deviceInfo: authInfo?.deviceInfo || '', - authType: AUTH_LOCAL_AUTHENTICATION_TYPE, - }, - }); + if (this.eventDispatchService) + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_LOCAL_AUTHENTICATION_TYPE, + }, + }); return response; } @@ -75,14 +78,17 @@ export class AuthLocalController { protected async dispatchAuthenticatedEvent( payload?: AuthenticatedEventInterface, ): Promise { - const authenticatedEventAsync = new AuthLocalAuthenticatedEventAsync( - payload, - ); + if (this.eventDispatchService) { + const authenticatedEventAsync = new AuthLocalAuthenticatedEventAsync( + payload, + ); - const eventResult = await this.eventDispatchService.async( - authenticatedEventAsync, - ); + const eventResult = await this.eventDispatchService.async( + authenticatedEventAsync, + ); - return eventResult.every((it) => it === true); + return eventResult.every((it) => it === true); + } + return true; } } diff --git a/packages/nestjs-common/src/decorators/auth-info.decorator.ts b/packages/nestjs-common/src/decorators/auth-info.decorator.ts index 04ae28f41..3e61a8918 100644 --- a/packages/nestjs-common/src/decorators/auth-info.decorator.ts +++ b/packages/nestjs-common/src/decorators/auth-info.decorator.ts @@ -8,9 +8,11 @@ export const AuthInfo = createParamDecorator( ): AuthenticatedUserInfoInterface | string | undefined => { const request = ctx.switchToHttp().getRequest(); const ipAddress = + request.ip || request.connection?.ip || request.headers['x-forwarded-for'] || request.connection?.remoteAddress || + request.connection?.remote_addr || ''; const deviceInfo = request.headers['user-agent'] || ''; const userLoginInfo: AuthenticatedUserInfoInterface = { From 34e36912626f759851a20e8012ebd7001b560a32 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Sat, 25 Jan 2025 18:00:23 -0300 Subject: [PATCH 5/7] chore: yarn update --- yarn.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yarn.lock b/yarn.lock index 2b221256d..674efb347 100644 --- a/yarn.lock +++ b/yarn.lock @@ -819,6 +819,7 @@ __metadata: "@concepta/nestjs-authentication": "npm:^6.0.0-alpha.0" "@concepta/nestjs-common": "npm:^6.0.0-alpha.0" "@concepta/nestjs-crud": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-event": "npm:^6.0.0-alpha.0" "@concepta/nestjs-exception": "npm:^6.0.0-alpha.0" "@concepta/nestjs-federated": "npm:^6.0.0-alpha.0" "@concepta/nestjs-jwt": "npm:^6.0.0-alpha.0" @@ -849,6 +850,7 @@ __metadata: "@concepta/nestjs-authentication": "npm:^6.0.0-alpha.0" "@concepta/nestjs-common": "npm:^6.0.0-alpha.0" "@concepta/nestjs-crud": "npm:^6.0.0-alpha.0" + "@concepta/nestjs-event": "npm:^6.0.0-alpha.0" "@concepta/nestjs-exception": "npm:^6.0.0-alpha.0" "@concepta/nestjs-federated": "npm:^6.0.0-alpha.0" "@concepta/nestjs-jwt": "npm:^6.0.0-alpha.0" From d7c8f85af1ae0c9372a1972df71fb07b72ff8fec Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Tue, 28 Jan 2025 17:17:15 -0300 Subject: [PATCH 6/7] chore: update file name --- packages/nestjs-auth-google/src/auth-google.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs-auth-google/src/auth-google.controller.ts b/packages/nestjs-auth-google/src/auth-google.controller.ts index 4b2ae30b1..723ef9778 100644 --- a/packages/nestjs-auth-google/src/auth-google.controller.ts +++ b/packages/nestjs-auth-google/src/auth-google.controller.ts @@ -19,7 +19,7 @@ import { } from './auth-google.constants'; import { AuthGoogleGuard } from './auth-google.guard'; import { EventDispatchService } from '@concepta/nestjs-event'; -import { AuthGoogleAuthenticatedEventAsync } from './events/auth-GOOGLE-authenticated.event'; +import { AuthGoogleAuthenticatedEventAsync } from './events/auth-google-authenticated.event'; /** * Google controller From f60e8fd9ea62270d55df7bc07acc9f66a9a1a07a Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Wed, 29 Jan 2025 17:16:34 -0300 Subject: [PATCH 7/7] chore: update events --- .../src/auth-apple.controller.ts | 45 ++++++------------- .../src/auth-github.controller.ts | 38 ++++++---------- .../src/auth-google.controller.ts | 38 ++++++---------- .../src/listeners/authenticated-listener.ts | 21 ++------- 4 files changed, 46 insertions(+), 96 deletions(-) diff --git a/packages/nestjs-auth-apple/src/auth-apple.controller.ts b/packages/nestjs-auth-apple/src/auth-apple.controller.ts index c13558df4..4d6e8cdfd 100644 --- a/packages/nestjs-auth-apple/src/auth-apple.controller.ts +++ b/packages/nestjs-auth-apple/src/auth-apple.controller.ts @@ -1,11 +1,4 @@ -import { - Controller, - Inject, - Get, - UseGuards, - Post, - Optional, -} from '@nestjs/common'; +import { Controller, Inject, Get, UseGuards, Post } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AuthenticatedEventInterface, @@ -26,7 +19,6 @@ import { } from './auth-apple.constants'; import { AuthAppleGuard } from './auth-apple.guard'; import { AuthAppleAuthenticatedEventAsync } from './events/auth-apple-authenticated.event'; -import { EventDispatchService } from '@concepta/nestjs-event'; /** * Apple controller @@ -51,9 +43,6 @@ export class AuthAppleController { constructor( @Inject(AUTH_APPLE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, - @Optional() - @Inject(EventDispatchService) - private readonly eventDispatchService?: EventDispatchService, ) {} /** @@ -79,15 +68,14 @@ export class AuthAppleController { ): Promise { const response = this.issueTokenService.responsePayload(user.id); - if (this.eventDispatchService) - await this.dispatchAuthenticatedEvent({ - userInfo: { - userId: user.id, - ipAddress: authInfo?.ipAddress || '', - deviceInfo: authInfo?.deviceInfo || '', - authType: AUTH_APPLE_AUTHENTICATION_TYPE, - }, - }); + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_APPLE_AUTHENTICATION_TYPE, + }, + }); return response; } @@ -95,17 +83,12 @@ export class AuthAppleController { protected async dispatchAuthenticatedEvent( payload?: AuthenticatedEventInterface, ): Promise { - if (this.eventDispatchService) { - const authenticatedEventAsync = new AuthAppleAuthenticatedEventAsync( - payload, - ); + const authenticatedEventAsync = new AuthAppleAuthenticatedEventAsync( + payload, + ); - const eventResult = await this.eventDispatchService.async( - authenticatedEventAsync, - ); + const eventResult = await authenticatedEventAsync.emit(); - return eventResult.every((it) => it === true); - } - return true; + return eventResult.every((it) => it === true); } } diff --git a/packages/nestjs-auth-github/src/auth-github.controller.ts b/packages/nestjs-auth-github/src/auth-github.controller.ts index 48c49daf6..b2051451b 100644 --- a/packages/nestjs-auth-github/src/auth-github.controller.ts +++ b/packages/nestjs-auth-github/src/auth-github.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Inject, Get, UseGuards, Optional } from '@nestjs/common'; +import { Controller, Inject, Get, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AuthenticatedEventInterface, @@ -18,7 +18,6 @@ import { AUTH_GITHUB_ISSUE_TOKEN_SERVICE_TOKEN, } from './auth-github.constants'; import { AuthGithubGuard } from './auth-github.guard'; -import { EventDispatchService } from '@concepta/nestjs-event'; import { AuthGithubAuthenticatedEventAsync } from './events/auth-github-authenticated.event'; // TODO: improve documentation @@ -45,9 +44,6 @@ export class AuthGithubController { constructor( @Inject(AUTH_GITHUB_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, - @Optional() - @Inject(EventDispatchService) - private readonly eventDispatchService: EventDispatchService, ) {} /** @@ -74,15 +70,14 @@ export class AuthGithubController { ): Promise { const response = this.issueTokenService.responsePayload(user.id); - if (this.eventDispatchService) - await this.dispatchAuthenticatedEvent({ - userInfo: { - userId: user.id, - ipAddress: authInfo?.ipAddress || '', - deviceInfo: authInfo?.deviceInfo || '', - authType: AUTH_GITHUB_AUTHENTICATION_TYPE, - }, - }); + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_GITHUB_AUTHENTICATION_TYPE, + }, + }); return response; } @@ -90,17 +85,12 @@ export class AuthGithubController { protected async dispatchAuthenticatedEvent( payload?: AuthenticatedEventInterface, ): Promise { - if (this.eventDispatchService) { - const authenticatedEventAsync = new AuthGithubAuthenticatedEventAsync( - payload, - ); + const authenticatedEventAsync = new AuthGithubAuthenticatedEventAsync( + payload, + ); - const eventResult = await this.eventDispatchService.async( - authenticatedEventAsync, - ); + const eventResult = await authenticatedEventAsync.emit(); - return eventResult.every((it) => it === true); - } - return true; + return eventResult.every((it) => it === true); } } diff --git a/packages/nestjs-auth-google/src/auth-google.controller.ts b/packages/nestjs-auth-google/src/auth-google.controller.ts index 723ef9778..238bbb0ba 100644 --- a/packages/nestjs-auth-google/src/auth-google.controller.ts +++ b/packages/nestjs-auth-google/src/auth-google.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Inject, Get, UseGuards, Optional } from '@nestjs/common'; +import { Controller, Inject, Get, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AuthenticatedEventInterface, @@ -18,7 +18,6 @@ import { AUTH_GOOGLE_ISSUE_TOKEN_SERVICE_TOKEN, } from './auth-google.constants'; import { AuthGoogleGuard } from './auth-google.guard'; -import { EventDispatchService } from '@concepta/nestjs-event'; import { AuthGoogleAuthenticatedEventAsync } from './events/auth-google-authenticated.event'; /** @@ -44,9 +43,6 @@ export class AuthGoogleController { constructor( @Inject(AUTH_GOOGLE_ISSUE_TOKEN_SERVICE_TOKEN) private issueTokenService: IssueTokenServiceInterface, - @Optional() - @Inject(EventDispatchService) - private readonly eventDispatchService?: EventDispatchService, ) {} /** @@ -72,15 +68,14 @@ export class AuthGoogleController { ): Promise { const response = this.issueTokenService.responsePayload(user.id); - if (this.eventDispatchService) - await this.dispatchAuthenticatedEvent({ - userInfo: { - userId: user.id, - ipAddress: authInfo?.ipAddress || '', - deviceInfo: authInfo?.deviceInfo || '', - authType: AUTH_GOOGLE_AUTHENTICATION_TYPE, - }, - }); + await this.dispatchAuthenticatedEvent({ + userInfo: { + userId: user.id, + ipAddress: authInfo?.ipAddress || '', + deviceInfo: authInfo?.deviceInfo || '', + authType: AUTH_GOOGLE_AUTHENTICATION_TYPE, + }, + }); return response; } @@ -88,17 +83,12 @@ export class AuthGoogleController { protected async dispatchAuthenticatedEvent( payload?: AuthenticatedEventInterface, ): Promise { - if (this.eventDispatchService) { - const authenticatedEventAsync = new AuthGoogleAuthenticatedEventAsync( - payload, - ); + const authenticatedEventAsync = new AuthGoogleAuthenticatedEventAsync( + payload, + ); - const eventResult = await this.eventDispatchService.async( - authenticatedEventAsync, - ); + const eventResult = await authenticatedEventAsync.emit(); - return eventResult.every((it) => it === true); - } - return true; + return eventResult.every((it) => it === true); } } diff --git a/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts b/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts index fba6d54fb..aad708e4d 100644 --- a/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts +++ b/packages/nestjs-auth-history/src/listeners/authenticated-listener.ts @@ -1,16 +1,6 @@ import { AuthenticatedEventInterface } from '@concepta/nestjs-common'; -import { - EventAsyncInterface, - EventListenerOn, - EventListenService, -} from '@concepta/nestjs-event'; -import { - Inject, - Injectable, - Logger, - OnModuleInit, - Optional, -} from '@nestjs/common'; +import { EventAsyncInterface, EventListenerOn } from '@concepta/nestjs-event'; +import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { AUTH_HISTORY_MODULE_SETTINGS_TOKEN } from '../auth-history.constants'; import { AuthHistoryMutateServiceInterface } from '../interfaces/auth-history-mutate-service.interface'; import { AuthHistorySettingsInterface } from '../interfaces/auth-history-settings.interface'; @@ -28,17 +18,14 @@ export class AuthenticatedListener private settings: AuthHistorySettingsInterface, @Inject(AuthHistoryMutateService) private authHistoryMutateService: AuthHistoryMutateServiceInterface, - @Optional() - @Inject(EventListenService) - private eventListenService?: EventListenService, ) { super(); } onModuleInit() { - if (this.eventListenService && this.settings.authenticatedEvents) { + if (this.settings.authenticatedEvents) { this.settings.authenticatedEvents.forEach((event) => { - this.eventListenService?.on(event, this); + this.on(event); }); } }