diff --git a/packages/nestjs-auth-local/src/__fixtures__/user/constants.ts b/packages/nestjs-auth-local/src/__fixtures__/user/constants.ts index 7c8d39c3a..bd2184958 100644 --- a/packages/nestjs-auth-local/src/__fixtures__/user/constants.ts +++ b/packages/nestjs-auth-local/src/__fixtures__/user/constants.ts @@ -8,6 +8,7 @@ export const LOGIN_SUCCESS = { export const USER_SUCCESS: AuthLocalCredentialsInterface = { id: randomUUID(), + active: true, passwordHash: LOGIN_SUCCESS.password, passwordSalt: LOGIN_SUCCESS.password, username: LOGIN_SUCCESS.username, diff --git a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts index e61e9ada0..d6ef80693 100644 --- a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts +++ b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts @@ -5,6 +5,7 @@ export class UserFixture { id!: string; username!: string; + active!: boolean; password!: string; passwordHash!: string | null; passwordSalt!: string | null; diff --git a/packages/nestjs-auth-local/src/auth-local.constants.ts b/packages/nestjs-auth-local/src/auth-local.constants.ts index 8d0e1c181..64f4c036b 100644 --- a/packages/nestjs-auth-local/src/auth-local.constants.ts +++ b/packages/nestjs-auth-local/src/auth-local.constants.ts @@ -1,6 +1,9 @@ export const AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN = 'AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN'; +export const AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN = + 'AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN'; + export const AUTH_LOCAL_MODULE_SETTINGS_TOKEN = 'AUTH_LOCAL_MODULE_SETTINGS_TOKEN'; @@ -10,4 +13,7 @@ export const AUTH_LOCAL_MODULE_DEFAULT_SETTINGS_TOKEN = export const AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN = 'AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN'; +export const AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN = + 'AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN'; + export const AUTH_LOCAL_STRATEGY_NAME = 'local'; diff --git a/packages/nestjs-auth-local/src/auth-local.module-definition.ts b/packages/nestjs-auth-local/src/auth-local.module-definition.ts index 30bfead5e..43aee98c8 100644 --- a/packages/nestjs-auth-local/src/auth-local.module-definition.ts +++ b/packages/nestjs-auth-local/src/auth-local.module-definition.ts @@ -6,7 +6,10 @@ import { import { ConfigModule } from '@nestjs/config'; import { createSettingsProvider } from '@concepta/nestjs-common'; -import { PasswordStorageService } from '@concepta/nestjs-password'; +import { + PasswordStorageService, + PasswordStorageServiceInterface, +} from '@concepta/nestjs-password'; import { IssueTokenService, IssueTokenServiceInterface, @@ -14,8 +17,10 @@ import { import { AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN, AUTH_LOCAL_MODULE_SETTINGS_TOKEN, AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, } from './auth-local.constants'; import { AuthLocalOptionsExtrasInterface } from './interfaces/auth-local-options-extras.interface'; @@ -24,6 +29,8 @@ import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.int import { authLocalDefaultConfig } from './config/auth-local-default.config'; import { AuthLocalController } from './auth-local.controller'; import { AuthLocalStrategy } from './auth-local.strategy'; +import { AuthLocalValidateUserService } from './services/auth-local-validate-user.service'; +import { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface'; const RAW_OPTIONS_TOKEN = Symbol('__AUTH_LOCAL_MODULE_RAW_OPTIONS_TOKEN__'); @@ -73,6 +80,8 @@ export function createAuthLocalExports(): string[] { AUTH_LOCAL_MODULE_SETTINGS_TOKEN, AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN, ]; } @@ -85,9 +94,12 @@ export function createAuthLocalProviders(options: { IssueTokenService, PasswordStorageService, AuthLocalStrategy, + AuthLocalValidateUserService, createAuthLocalOptionsProvider(options.overrides), + createAuthLocalValidateUserServiceProvider(options.overrides), createAuthLocalIssueTokenServiceProvider(options.overrides), createAuthLocalUserLookupServiceProvider(options.overrides), + createAuthLocalPasswordStorageServiceProvider(options.overrides), ]; } @@ -113,6 +125,30 @@ export function createAuthLocalOptionsProvider( }); } +export function createAuthLocalValidateUserServiceProvider( + optionsOverrides?: AuthLocalOptions, +): Provider { + return { + provide: AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, + inject: [ + RAW_OPTIONS_TOKEN, + AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN, + ], + useFactory: async ( + options: AuthLocalOptionsInterface, + userLookupService: AuthLocalUserLookupServiceInterface, + passwordStorageService: PasswordStorageServiceInterface, + ) => + optionsOverrides?.validateUserService ?? + options.validateUserService ?? + new AuthLocalValidateUserService( + userLookupService, + passwordStorageService, + ), + }; +} + export function createAuthLocalIssueTokenServiceProvider( optionsOverrides?: AuthLocalOptions, ): Provider { @@ -129,6 +165,22 @@ export function createAuthLocalIssueTokenServiceProvider( }; } +export function createAuthLocalPasswordStorageServiceProvider( + optionsOverrides?: AuthLocalOptions, +): Provider { + return { + provide: AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN, + inject: [RAW_OPTIONS_TOKEN, PasswordStorageService], + useFactory: async ( + options: AuthLocalOptionsInterface, + defaultService: PasswordStorageServiceInterface, + ) => + optionsOverrides?.passwordStorageService ?? + options.passwordStorageService ?? + defaultService, + }; +} + export function createAuthLocalUserLookupServiceProvider( optionsOverrides?: AuthLocalOptions, ): Provider { 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 6ae44ffc2..1d73b937a 100644 --- a/packages/nestjs-auth-local/src/auth-local.module.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.module.spec.ts @@ -17,6 +17,10 @@ import { IssueTokenService, IssueTokenServiceInterface, } from '@concepta/nestjs-authentication'; +import { + PasswordStorageService, + PasswordStorageServiceInterface, +} from '@concepta/nestjs-password'; import { AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, @@ -30,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'; describe(AuthLocalModule, () => { const jwtAccessService = new NestJwtService(); @@ -43,7 +48,9 @@ describe(AuthLocalModule, () => { let testModule: TestingModule; let authLocalModule: AuthLocalModule; let userLookupService: AuthLocalUserLookupServiceInterface; + let validateUserService: AuthLocalUserLookupServiceInterface; let issueTokenService: IssueTokenServiceInterface; + let passwordStorageService: PasswordStorageServiceInterface; describe(AuthLocalModule.forRoot, () => { beforeEach(async () => { @@ -194,13 +201,17 @@ describe(AuthLocalModule, () => { function commonVars(module: TestingModule) { authLocalModule = module.get(AuthLocalModule); userLookupService = module.get(UserLookupServiceFixture); + validateUserService = module.get(AuthLocalValidateUserService); issueTokenService = module.get(IssueTokenService); + passwordStorageService = module.get(PasswordStorageService); } function commonTests() { expect(authLocalModule).toBeInstanceOf(AuthLocalModule); expect(userLookupService).toBeInstanceOf(UserLookupServiceFixture); expect(issueTokenService).toBeInstanceOf(IssueTokenService); + expect(passwordStorageService).toBeInstanceOf(PasswordStorageService); + expect(validateUserService).toBeInstanceOf(AuthLocalValidateUserService); } }); diff --git a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts index acdb314e8..8d62152b0 100644 --- a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts @@ -5,6 +5,9 @@ import { mock } from 'jest-mock-extended'; import { AuthLocalStrategy } from './auth-local.strategy'; import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface'; import { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface'; +import { AuthLocalValidateUserServiceInterface } from './interfaces/auth-local-validate-user-service.interface'; +import { AuthLocalValidateUserService } from './services/auth-local-validate-user.service'; + import { UserFixture } from './__fixtures__/user/user.entity.fixture'; describe(AuthLocalStrategy, () => { @@ -14,6 +17,7 @@ describe(AuthLocalStrategy, () => { let user: UserFixture; let settings: AuthLocalSettingsInterface; let userLookUpService: AuthLocalUserLookupServiceInterface; + let validateUserService: AuthLocalValidateUserServiceInterface; let passwordStorageService: PasswordStorageService; let authLocalStrategy: AuthLocalStrategy; @@ -26,14 +30,15 @@ describe(AuthLocalStrategy, () => { userLookUpService = mock(); passwordStorageService = mock(); - authLocalStrategy = new AuthLocalStrategy( - settings, + validateUserService = new AuthLocalValidateUserService( userLookUpService, passwordStorageService, ); + authLocalStrategy = new AuthLocalStrategy(settings, validateUserService); user = new UserFixture(); user.id = randomUUID(); + user.active = true; jest.spyOn(userLookUpService, 'byUsername').mockResolvedValue(user); }); @@ -41,11 +46,7 @@ describe(AuthLocalStrategy, () => { settings = mock>({ loginDto: undefined, }); - authLocalStrategy = new AuthLocalStrategy( - settings, - userLookUpService, - passwordStorageService, - ); + authLocalStrategy = new AuthLocalStrategy(settings, validateUserService); expect(true).toBeTruthy(); }); @@ -95,11 +96,7 @@ describe(AuthLocalStrategy, () => { usernameField: USERNAME, passwordField: PASSWORD, }); - authLocalStrategy = new AuthLocalStrategy( - settings, - userLookUpService, - passwordStorageService, - ); + authLocalStrategy = new AuthLocalStrategy(settings, validateUserService); const t = () => authLocalStrategy['assertSettings'](); expect(t).toThrowError(); }); @@ -110,25 +107,18 @@ describe(AuthLocalStrategy, () => { usernameField: undefined, passwordField: PASSWORD, }); - authLocalStrategy = new AuthLocalStrategy( - settings, - userLookUpService, - passwordStorageService, - ); + authLocalStrategy = new AuthLocalStrategy(settings, validateUserService); const t = () => authLocalStrategy['assertSettings'](); expect(t).toThrowError(); }); + it('should throw error for no passwordField', async () => { settings = mock>({ loginDto: UserFixture, usernameField: USERNAME, passwordField: undefined, }); - authLocalStrategy = new AuthLocalStrategy( - settings, - userLookUpService, - passwordStorageService, - ); + authLocalStrategy = new AuthLocalStrategy(settings, validateUserService); const t = () => authLocalStrategy['assertSettings'](); expect(t).toThrowError(); }); diff --git a/packages/nestjs-auth-local/src/auth-local.strategy.ts b/packages/nestjs-auth-local/src/auth-local.strategy.ts index afca3bdf1..d47215467 100644 --- a/packages/nestjs-auth-local/src/auth-local.strategy.ts +++ b/packages/nestjs-auth-local/src/auth-local.strategy.ts @@ -6,18 +6,17 @@ import { Injectable, UnauthorizedException, } from '@nestjs/common'; -import { ReferenceUsername } from '@concepta/ts-core'; +import { ReferenceIdInterface, ReferenceUsername } from '@concepta/ts-core'; import { PassportStrategyFactory } from '@concepta/nestjs-authentication'; -import { PasswordStorageService } from '@concepta/nestjs-password'; import { AUTH_LOCAL_MODULE_SETTINGS_TOKEN, - AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, AUTH_LOCAL_STRATEGY_NAME, } from './auth-local.constants'; import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface'; -import { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface'; +import { AuthLocalValidateUserServiceInterface } from './interfaces/auth-local-validate-user-service.interface'; /** * Define the Local strategy using passport. @@ -34,14 +33,13 @@ export class AuthLocalStrategy extends PassportStrategyFactory( * * @param userLookupService The service used to get the user * @param settings The settings for the local strategy - * @param passwordService The service used to hash and validate passwords + * @param passwordStorageService The service used to hash and validate passwords */ constructor( @Inject(AUTH_LOCAL_MODULE_SETTINGS_TOKEN) private settings: AuthLocalSettingsInterface, - @Inject(AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN) - private userLookupService: AuthLocalUserLookupServiceInterface, - private passwordService: PasswordStorageService, + @Inject(AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN) + private validateUserService: AuthLocalValidateUserServiceInterface, ) { super({ usernameField: settings?.usernameField, @@ -71,21 +69,24 @@ export class AuthLocalStrategy extends PassportStrategyFactory( throw new BadRequestException(e); } - const user = await this.userLookupService.byUsername(dto[usernameField]); + let validatedUser: ReferenceIdInterface; - if (!user) { + try { + // try to get fully validated user + validatedUser = await this.validateUserService.validateUser({ + username, + password, + }); + // did we get a valid user? + if (!validatedUser) { + throw new Error(`No valid user found: ${username}`); + } + } catch (e) { + // TODO: maybe log original? throw new UnauthorizedException(); } - // validate password - const isValid = await this.passwordService.validateObject( - dto[passwordField], - user, - ); - - if (!isValid) throw new UnauthorizedException(); - - return user; + return validatedUser; } /** diff --git a/packages/nestjs-auth-local/src/index.ts b/packages/nestjs-auth-local/src/index.ts index 9c9e88020..6f8a60d6f 100644 --- a/packages/nestjs-auth-local/src/index.ts +++ b/packages/nestjs-auth-local/src/index.ts @@ -1,4 +1,6 @@ +export { AuthLocalValidateUserInterface } from './interfaces/auth-local-validate-user.interface'; export { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface'; +export { AuthLocalValidateUserServiceInterface } from './interfaces/auth-local-validate-user-service.interface'; export * from './auth-local.module'; export * from './auth-local.controller'; diff --git a/packages/nestjs-auth-local/src/interfaces/auth-local-credentials.interface.ts b/packages/nestjs-auth-local/src/interfaces/auth-local-credentials.interface.ts index f8566ab43..816692ff6 100644 --- a/packages/nestjs-auth-local/src/interfaces/auth-local-credentials.interface.ts +++ b/packages/nestjs-auth-local/src/interfaces/auth-local-credentials.interface.ts @@ -1,4 +1,5 @@ import { + ReferenceActiveInterface, ReferenceIdInterface, ReferenceUsernameInterface, } from '@concepta/ts-core'; @@ -10,4 +11,5 @@ import { PasswordStorageInterface } from '@concepta/nestjs-password'; export interface AuthLocalCredentialsInterface extends ReferenceIdInterface, ReferenceUsernameInterface, + ReferenceActiveInterface, PasswordStorageInterface {} diff --git a/packages/nestjs-auth-local/src/interfaces/auth-local-options.interface.ts b/packages/nestjs-auth-local/src/interfaces/auth-local-options.interface.ts index 5cd8cce9f..fb837992f 100644 --- a/packages/nestjs-auth-local/src/interfaces/auth-local-options.interface.ts +++ b/packages/nestjs-auth-local/src/interfaces/auth-local-options.interface.ts @@ -1,6 +1,8 @@ import { IssueTokenServiceInterface } from '@concepta/nestjs-authentication'; +import { PasswordStorageServiceInterface } from '@concepta/nestjs-password'; import { AuthLocalSettingsInterface } from './auth-local-settings.interface'; import { AuthLocalUserLookupServiceInterface } from './auth-local-user-lookup-service.interface'; +import { AuthLocalValidateUserServiceInterface } from './auth-local-validate-user-service.interface'; export interface AuthLocalOptionsInterface { /** @@ -13,6 +15,16 @@ export interface AuthLocalOptionsInterface { */ issueTokenService?: IssueTokenServiceInterface; + /** + * Implementation of a class to validate user + */ + validateUserService?: AuthLocalValidateUserServiceInterface; + + /** + * Implementation of a class to handle password storage + */ + passwordStorageService?: PasswordStorageServiceInterface; + /** * Settings */ diff --git a/packages/nestjs-auth-local/src/interfaces/auth-local-validate-user-service.interface.ts b/packages/nestjs-auth-local/src/interfaces/auth-local-validate-user-service.interface.ts new file mode 100644 index 000000000..bb4ebde3d --- /dev/null +++ b/packages/nestjs-auth-local/src/interfaces/auth-local-validate-user-service.interface.ts @@ -0,0 +1,10 @@ +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { ValidateUserServiceInterface } from '@concepta/nestjs-authentication'; +import { AuthLocalValidateUserInterface } from './auth-local-validate-user.interface'; + +export interface AuthLocalValidateUserServiceInterface + extends ValidateUserServiceInterface<[AuthLocalValidateUserInterface]> { + validateUser: ( + dto: AuthLocalValidateUserInterface, + ) => Promise; +} diff --git a/packages/nestjs-auth-local/src/interfaces/auth-local-validate-user.interface.ts b/packages/nestjs-auth-local/src/interfaces/auth-local-validate-user.interface.ts new file mode 100644 index 000000000..12490a2fe --- /dev/null +++ b/packages/nestjs-auth-local/src/interfaces/auth-local-validate-user.interface.ts @@ -0,0 +1,4 @@ +export interface AuthLocalValidateUserInterface { + username: string; + password: string; +} diff --git a/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.ts b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.ts new file mode 100644 index 000000000..348dc30f6 --- /dev/null +++ b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.ts @@ -0,0 +1,60 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { ValidateUserService } from '@concepta/nestjs-authentication'; +import { PasswordStorageServiceInterface } from '@concepta/nestjs-password'; +import { + AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, +} from '../auth-local.constants'; +import { AuthLocalValidateUserInterface } from '../interfaces/auth-local-validate-user.interface'; +import { AuthLocalValidateUserServiceInterface } from '../interfaces/auth-local-validate-user-service.interface'; +import { AuthLocalUserLookupServiceInterface } from '../interfaces/auth-local-user-lookup-service.interface'; + +@Injectable() +export class AuthLocalValidateUserService + extends ValidateUserService<[AuthLocalValidateUserInterface]> + implements AuthLocalValidateUserServiceInterface +{ + constructor( + @Inject(AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN) + private userLookupService: AuthLocalUserLookupServiceInterface, + @Inject(AUTH_LOCAL_MODULE_PASSWORD_STORAGE_SERVICE_TOKEN) + private passwordStorageService: PasswordStorageServiceInterface, + ) { + super(); + } + + /** + * Returns true if user is considered valid for authentication purposes. + */ + async validateUser( + dto: AuthLocalValidateUserInterface, + ): Promise { + // try to get the user by username + const user = await this.userLookupService.byUsername(dto.username); + + // did we get a user? + if (!user) { + throw new Error(`No user found for username: ${dto.username}`); + } + + // is the user active? + if (!this.isActive(user)) { + throw new Error(`User with username '${dto.username}' is inactive`); + } + + // validate password + const isValid = await this.passwordStorageService.validateObject( + dto.password, + user, + ); + + // password is valid? + if (!isValid) { + throw new Error(`Invalid password for username: ${user.username}`); + } + + // return the user + return user; + } +} diff --git a/packages/nestjs-authentication/src/index.ts b/packages/nestjs-authentication/src/index.ts index 84e13de61..fb5c79ecd 100644 --- a/packages/nestjs-authentication/src/index.ts +++ b/packages/nestjs-authentication/src/index.ts @@ -7,7 +7,8 @@ export * from './decorators/auth-user.decorator'; export * from './interfaces/authentication-options.interface'; export { VerifyTokenServiceInterface } from './interfaces/verify-token-service.interface'; -export * from './interfaces/issue-token-service.interface'; +export { IssueTokenServiceInterface } from './interfaces/issue-token-service.interface'; +export { ValidateUserServiceInterface } from './interfaces/validate-user-service.interface'; export * from './factories/passport-strategy.factory'; export * from './guards/auth.guard'; @@ -16,3 +17,4 @@ export { AuthenticationJwtResponseDto } from './dto/authentication-jwt-response. export { IssueTokenService } from './services/issue-token.service'; export { VerifyTokenService } from './services/verify-token.service'; +export { ValidateUserService } from './services/validate-user.service'; diff --git a/packages/nestjs-authentication/src/interfaces/validate-user-service.interface.ts b/packages/nestjs-authentication/src/interfaces/validate-user-service.interface.ts new file mode 100644 index 000000000..bf84fd986 --- /dev/null +++ b/packages/nestjs-authentication/src/interfaces/validate-user-service.interface.ts @@ -0,0 +1,12 @@ +import { + ReferenceActiveInterface, + ReferenceIdInterface, +} from '@concepta/ts-core'; + +export interface ValidateUserServiceInterface< + T extends unknown[] = unknown[], + R extends ReferenceIdInterface = ReferenceIdInterface, +> { + validateUser: (..._: T) => Promise; + isActive: (user: R & ReferenceActiveInterface) => Promise; +} diff --git a/packages/nestjs-authentication/src/services/validate-user.service.ts b/packages/nestjs-authentication/src/services/validate-user.service.ts new file mode 100644 index 000000000..4d37f9a17 --- /dev/null +++ b/packages/nestjs-authentication/src/services/validate-user.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { + ReferenceActiveInterface, + ReferenceIdInterface, +} from '@concepta/ts-core'; +import { ValidateUserServiceInterface } from '../interfaces/validate-user-service.interface'; + +@Injectable() +export abstract class ValidateUserService< + T extends unknown[] = unknown[], + R extends ReferenceIdInterface = ReferenceIdInterface, +> implements ValidateUserServiceInterface +{ + /** + * Returns validated user + */ + abstract validateUser(...rest: T): Promise; + + /** + * Returns true if user is considered valid for authentication purposes. + */ + async isActive( + user: ReferenceIdInterface & ReferenceActiveInterface, + ): Promise { + return user.active === true; + } +} diff --git a/packages/nestjs-password/src/index.ts b/packages/nestjs-password/src/index.ts index fe0973beb..507d2125a 100644 --- a/packages/nestjs-password/src/index.ts +++ b/packages/nestjs-password/src/index.ts @@ -8,4 +8,5 @@ export * from './services/password-strength.service'; export * from './interfaces/password-options.interface'; export * from './interfaces/password-storage.interface'; +export * from './interfaces/password-storage-service.interface'; export * from './interfaces/password-creation-service.interface'; diff --git a/packages/nestjs-samples/src/03-authentication/user/create-user-repository.ts b/packages/nestjs-samples/src/03-authentication/user/create-user-repository.ts index f8e6afb38..85c17f6dd 100644 --- a/packages/nestjs-samples/src/03-authentication/user/create-user-repository.ts +++ b/packages/nestjs-samples/src/03-authentication/user/create-user-repository.ts @@ -11,6 +11,7 @@ export function createUserRepository(dataSource: DataSource) { id: '1', email: 'first_user@dispostable.com', username: 'first_user', + active: true, // hashed for AS12378 passwordHash: '$2b$10$9y97gOLiusyKnzu7LRdMmOCVpp/xwddaa8M6KtgenvUDao5I.8mJS', @@ -26,6 +27,7 @@ export function createUserRepository(dataSource: DataSource) { id: '2', email: 'second_user@dispostable.com', username: 'second_user', + active: true, // hashed for AS12378 passwordHash: '$2b$10$9y97gOLiusyKnzu7LRdMmOCVpp/xwddaa8M6KtgenvUDao5I.8mJS', diff --git a/packages/nestjs-user/src/__fixtures__/create-user-repository.fixture.ts b/packages/nestjs-user/src/__fixtures__/create-user-repository.fixture.ts index 76756fb42..d8a3ac612 100644 --- a/packages/nestjs-user/src/__fixtures__/create-user-repository.fixture.ts +++ b/packages/nestjs-user/src/__fixtures__/create-user-repository.fixture.ts @@ -11,6 +11,7 @@ export function createUserRepositoryFixture(dataSource: DataSource) { id: '1', email: 'first_user@dispostable.com', username: 'first_user', + active: true, // hashed for AS12378 passwordHash: '$2b$10$9y97gOLiusyKnzu7LRdMmOCVpp/xwddaa8M6KtgenvUDao5I.8mJS', @@ -26,6 +27,7 @@ export function createUserRepositoryFixture(dataSource: DataSource) { id: '2', email: 'second_user@dispostable.com', username: 'second_user', + active: true, // hashed for AS12378 passwordHash: '$2b$10$9y97gOLiusyKnzu7LRdMmOCVpp/xwddaa8M6KtgenvUDao5I.8mJS', diff --git a/packages/nestjs-user/src/dto/user-create.dto.ts b/packages/nestjs-user/src/dto/user-create.dto.ts index d33407e8e..1df980c29 100644 --- a/packages/nestjs-user/src/dto/user-create.dto.ts +++ b/packages/nestjs-user/src/dto/user-create.dto.ts @@ -11,6 +11,7 @@ import { UserPasswordDto } from './user-password.dto'; export class UserCreateDto extends IntersectionType( PickType(UserDto, ['username', 'email'] as const), + PartialType(PickType(UserDto, ['active'] as const)), PartialType(UserPasswordDto), ) implements UserCreatableInterface {} diff --git a/packages/nestjs-user/src/dto/user-update.dto.ts b/packages/nestjs-user/src/dto/user-update.dto.ts index 182381174..36b4e835f 100644 --- a/packages/nestjs-user/src/dto/user-update.dto.ts +++ b/packages/nestjs-user/src/dto/user-update.dto.ts @@ -10,7 +10,7 @@ import { UserPasswordDto } from './user-password.dto'; @Exclude() export class UserUpdateDto extends IntersectionType( - PartialType(PickType(UserDto, ['email'] as const)), + PartialType(PickType(UserDto, ['email', 'active'] as const)), PartialType(UserPasswordDto), ) implements UserUpdatableInterface {} diff --git a/packages/nestjs-user/src/dto/user.dto.ts b/packages/nestjs-user/src/dto/user.dto.ts index 9ad5a8e4e..f1f44b796 100644 --- a/packages/nestjs-user/src/dto/user.dto.ts +++ b/packages/nestjs-user/src/dto/user.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsString, ValidateNested } from 'class-validator'; +import { IsBoolean, IsEmail, IsString, ValidateNested } from 'class-validator'; import { Exclude, Expose, Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; import { AuditInterface } from '@concepta/ts-core'; @@ -43,6 +43,17 @@ export class UserDto implements UserInterface { @IsString() username: string = ''; + /** + * Active + */ + @Expose() + @ApiProperty({ + type: 'string', + description: 'Active', + }) + @IsBoolean() + active!: boolean; + /** * Audit */ diff --git a/packages/nestjs-user/src/entities/user-postgres.entity.ts b/packages/nestjs-user/src/entities/user-postgres.entity.ts index ceab839a0..15a0e67c8 100644 --- a/packages/nestjs-user/src/entities/user-postgres.entity.ts +++ b/packages/nestjs-user/src/entities/user-postgres.entity.ts @@ -25,6 +25,12 @@ export abstract class UserPostgresEntity implements UserEntityInterface { @Column() username!: string; + /** + * Active + */ + @Column({ default: true }) + active!: boolean; + /** * Password hash */ diff --git a/packages/nestjs-user/src/entities/user-sqlite.entity.ts b/packages/nestjs-user/src/entities/user-sqlite.entity.ts index a6d39bbd3..88080617a 100644 --- a/packages/nestjs-user/src/entities/user-sqlite.entity.ts +++ b/packages/nestjs-user/src/entities/user-sqlite.entity.ts @@ -22,6 +22,12 @@ export abstract class UserSqliteEntity implements UserEntityInterface { @Column() username!: string; + /** + * Active + */ + @Column({ default: true }) + active!: boolean; + /** * Password hash */ diff --git a/packages/ts-common/src/user/interfaces/user-creatable.interface.ts b/packages/ts-common/src/user/interfaces/user-creatable.interface.ts index 5f2651520..257542366 100644 --- a/packages/ts-common/src/user/interfaces/user-creatable.interface.ts +++ b/packages/ts-common/src/user/interfaces/user-creatable.interface.ts @@ -3,4 +3,5 @@ import { PasswordPlainInterface } from '../../password/interfaces/password-plain export interface UserCreatableInterface extends Pick, + Partial>, Partial {} diff --git a/packages/ts-common/src/user/interfaces/user-updatable.interface.ts b/packages/ts-common/src/user/interfaces/user-updatable.interface.ts index 350eaa712..7f18f7654 100644 --- a/packages/ts-common/src/user/interfaces/user-updatable.interface.ts +++ b/packages/ts-common/src/user/interfaces/user-updatable.interface.ts @@ -1,4 +1,6 @@ import { UserCreatableInterface } from './user-creatable.interface'; export interface UserUpdatableInterface - extends Partial> {} + extends Partial< + Pick + > {} diff --git a/packages/ts-common/src/user/interfaces/user.interface.ts b/packages/ts-common/src/user/interfaces/user.interface.ts index 888777c39..c10e13397 100644 --- a/packages/ts-common/src/user/interfaces/user.interface.ts +++ b/packages/ts-common/src/user/interfaces/user.interface.ts @@ -1,4 +1,5 @@ import { + ReferenceActiveInterface, ReferenceAuditInterface, ReferenceEmailInterface, ReferenceIdInterface, @@ -9,4 +10,5 @@ export interface UserInterface extends ReferenceIdInterface, ReferenceEmailInterface, ReferenceUsernameInterface, + ReferenceActiveInterface, ReferenceAuditInterface {}