diff --git a/packages/nestjs-auth-local/package.json b/packages/nestjs-auth-local/package.json index 6bb658041..660e55cbc 100644 --- a/packages/nestjs-auth-local/package.json +++ b/packages/nestjs-auth-local/package.json @@ -14,6 +14,7 @@ "dependencies": { "@concepta/nestjs-authentication": "^5.0.0-alpha.4", "@concepta/nestjs-common": "^5.0.0-alpha.4", + "@concepta/nestjs-exception": "^5.0.0-alpha.4", "@concepta/nestjs-password": "^5.0.0-alpha.4", "@concepta/ts-common": "^5.0.0-alpha.4", "@concepta/ts-core": "^5.0.0-alpha.4", 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 f62dbac8e..a3664698e 100644 --- a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'crypto'; import { mock } from 'jest-mock-extended'; import { PasswordValidationService } from '@concepta/nestjs-password'; -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import { AuthLocalStrategy } from './auth-local.strategy'; import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface'; import { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface'; @@ -11,6 +11,8 @@ import { AuthLocalValidateUserService } from './services/auth-local-validate-use import { UserFixture } from './__fixtures__/user/user.entity.fixture'; import { ReferenceIdInterface } from '@concepta/ts-core'; import { AuthLocalValidateUserInterface } from './interfaces/auth-local-validate-user.interface'; +import { InvalidCredentialsException } from './exceptions/invalid-credentials.exception'; +import { InvalidLoginDataException } from './exceptions/invalid-login-data.exception'; describe(AuthLocalStrategy.name, () => { const USERNAME = 'username'; @@ -70,7 +72,7 @@ describe(AuthLocalStrategy.name, () => { }); const t = () => authLocalStrategy.validate(USERNAME, PASSWORD); - await expect(t).rejects.toThrow(UnauthorizedException); + await expect(t).rejects.toThrow(InvalidCredentialsException); }); it('should throw error on validateOrReject', async () => { @@ -85,14 +87,14 @@ describe(AuthLocalStrategy.name, () => { .mockRejectedValueOnce(BadRequestException); const t = () => authLocalStrategy.validate(USERNAME, PASSWORD); - await expect(t).rejects.toThrow(BadRequestException); + await expect(t).rejects.toThrow(InvalidLoginDataException); }); it('should return no user on userLookupService.byUsername', async () => { jest.spyOn(userLookUpService, 'byUsername').mockResolvedValue(null); const t = () => authLocalStrategy.validate(USERNAME, PASSWORD); - await expect(t).rejects.toThrow(UnauthorizedException); + await expect(t).rejects.toThrow(InvalidCredentialsException); }); it('should be invalid on passwordService.validateObject', async () => { @@ -101,7 +103,7 @@ describe(AuthLocalStrategy.name, () => { .mockResolvedValue(false); const t = () => authLocalStrategy.validate(USERNAME, PASSWORD); - await expect(t).rejects.toThrow(UnauthorizedException); + await expect(t).rejects.toThrow(InvalidCredentialsException); }); }); diff --git a/packages/nestjs-auth-local/src/auth-local.strategy.ts b/packages/nestjs-auth-local/src/auth-local.strategy.ts index 14429979e..90645481c 100644 --- a/packages/nestjs-auth-local/src/auth-local.strategy.ts +++ b/packages/nestjs-auth-local/src/auth-local.strategy.ts @@ -1,11 +1,6 @@ import { Strategy } from 'passport-local'; import { validateOrReject } from 'class-validator'; -import { - BadRequestException, - Inject, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { ReferenceIdInterface, ReferenceUsername } from '@concepta/ts-core'; import { PassportStrategyFactory } from '@concepta/nestjs-authentication'; @@ -17,6 +12,8 @@ import { import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface'; import { AuthLocalValidateUserServiceInterface } from './interfaces/auth-local-validate-user-service.interface'; +import { InvalidCredentialsException } from './exceptions/invalid-credentials.exception'; +import { InvalidLoginDataException } from './exceptions/invalid-login-data.exception'; /** * Define the Local strategy using passport. @@ -64,7 +61,9 @@ export class AuthLocalStrategy extends PassportStrategyFactory( try { await validateOrReject(dto); } catch (e) { - throw new BadRequestException(e); + throw new InvalidLoginDataException({ + originalError: e, + }); } let validatedUser: ReferenceIdInterface; @@ -81,7 +80,9 @@ export class AuthLocalStrategy extends PassportStrategyFactory( } } catch (e) { // TODO: maybe log original? - throw new UnauthorizedException(); + throw new InvalidCredentialsException({ + originalError: e, + }); } return validatedUser; diff --git a/packages/nestjs-auth-local/src/exceptions/invalid-credentials.exception.ts b/packages/nestjs-auth-local/src/exceptions/invalid-credentials.exception.ts new file mode 100644 index 000000000..31db71c09 --- /dev/null +++ b/packages/nestjs-auth-local/src/exceptions/invalid-credentials.exception.ts @@ -0,0 +1,17 @@ +import { HttpStatus } from '@nestjs/common'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; + +export class InvalidCredentialsException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'The provided credentials are incorrect. Please try again.', + httpStatus: HttpStatus.UNAUTHORIZED, + ...options, + }); + + this.errorCode = 'AUTH_LOCAL_INVALID_CREDENTIALS_ERROR'; + } +} diff --git a/packages/nestjs-auth-local/src/exceptions/invalid-login-data.exception.ts b/packages/nestjs-auth-local/src/exceptions/invalid-login-data.exception.ts new file mode 100644 index 000000000..f0fcb7008 --- /dev/null +++ b/packages/nestjs-auth-local/src/exceptions/invalid-login-data.exception.ts @@ -0,0 +1,18 @@ +import { HttpStatus } from '@nestjs/common'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; + +export class InvalidLoginDataException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: + 'The provided username or password is incorrect. Please try again.', + httpStatus: HttpStatus.BAD_REQUEST, + ...options, + }); + + this.errorCode = 'AUTH_LOCAL_INVALID_LOGIN_DATA_ERROR'; + } +} diff --git a/packages/nestjs-exception/src/constants/error-codes.constants.ts b/packages/nestjs-exception/src/constants/error-codes.constants.ts index 497131ebb..2108c89fd 100644 --- a/packages/nestjs-exception/src/constants/error-codes.constants.ts +++ b/packages/nestjs-exception/src/constants/error-codes.constants.ts @@ -2,13 +2,14 @@ export const ERROR_CODE_UNKNOWN = 'UNKNOWN'; export const ERROR_CODE_HTTP_UNKNOWN = 'HTTP_UNKNOWN'; export const ERROR_CODE_HTTP_BAD_REQUEST = 'HTTP_BAD_REQUEST'; export const ERROR_CODE_HTTP_NOT_FOUND = 'HTTP_NOT_FOUND'; +export const ERROR_CODE_HTTP_UNAUTHORIZED = 'HTTP_UNAUTHORIZED'; export const ERROR_CODE_HTTP_INTERNAL_SERVER_ERROR = 'HTTP_INTERNAL_SERVER_ERROR'; export const ERROR_MESSAGE_FALLBACK = 'Internal Server Error'; -// TODO: add remaining error codes export const HTTP_ERROR_CODE = new Map(); HTTP_ERROR_CODE.set(400, ERROR_CODE_HTTP_BAD_REQUEST); +HTTP_ERROR_CODE.set(401, ERROR_CODE_HTTP_UNAUTHORIZED); HTTP_ERROR_CODE.set(404, ERROR_CODE_HTTP_NOT_FOUND); HTTP_ERROR_CODE.set(500, ERROR_CODE_HTTP_INTERNAL_SERVER_ERROR); diff --git a/packages/nestjs-exception/src/index.ts b/packages/nestjs-exception/src/index.ts index 563b64ac4..40c0c49ef 100644 --- a/packages/nestjs-exception/src/index.ts +++ b/packages/nestjs-exception/src/index.ts @@ -10,3 +10,5 @@ export { RuntimeExceptionInterface } from './interfaces/runtime-exception.interf // exceptions export { RuntimeException } from './exceptions/runtime.exception'; +// utils +export { mapHttpStatus } from './utils/map-http-status.util'; diff --git a/packages/nestjs-logger-sentry/package.json b/packages/nestjs-logger-sentry/package.json index 742b05480..78d917fff 100644 --- a/packages/nestjs-logger-sentry/package.json +++ b/packages/nestjs-logger-sentry/package.json @@ -13,6 +13,7 @@ ], "dependencies": { "@concepta/nestjs-common": "^5.0.0-alpha.4", + "@concepta/nestjs-exception": "^5.0.0-alpha.4", "@concepta/nestjs-logger": "^5.0.0-alpha.4", "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", diff --git a/packages/nestjs-logger-sentry/src/interfaces/logger-sentry-extras.interface.ts b/packages/nestjs-logger-sentry/src/interfaces/logger-sentry-extras.interface.ts new file mode 100644 index 000000000..74a4aa80e --- /dev/null +++ b/packages/nestjs-logger-sentry/src/interfaces/logger-sentry-extras.interface.ts @@ -0,0 +1,10 @@ +import { RuntimeExceptionInterface } from '@concepta/nestjs-exception'; + +export interface LoggerSentryExtrasInterface + extends Partial< + Pick + > { + statusCode?: number; + message?: string | unknown; + originalError?: Error | string; +} diff --git a/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.spec.ts b/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.spec.ts index 7c01d5f92..0d6b70502 100644 --- a/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.spec.ts +++ b/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.spec.ts @@ -1,4 +1,4 @@ -import { LogLevel } from '@nestjs/common'; +import { BadRequestException, HttpStatus, LogLevel } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as Sentry from '@sentry/node'; @@ -6,6 +6,12 @@ import { LOGGER_SENTRY_MODULE_SETTINGS_TOKEN } from '../config/logger-sentry.con import { LoggerSentryConfigInterface } from '../interfaces/logger-sentry-config.interface'; import { LoggerSentrySettingsInterface } from '../interfaces/logger-sentry-settings.interface'; import { LoggerSentryTransport } from './logger-sentry.transport'; +import { + mapHttpStatus, + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; +import { isObject } from 'class-validator'; jest.mock('@sentry/node'); @@ -88,9 +94,8 @@ describe('loggerSentryTransport', () => { /** * Make sure call was made correctly - * */ - it('LoggerSentryTransport.correctValues', async () => { + it('LoggerSentryTransport.correctValues Error', async () => { const logLevel = 'log' as LogLevel; const error = new Error(); loggerSentryTransport.log(errorMessage, logLevel, error); @@ -100,7 +105,92 @@ describe('loggerSentryTransport', () => { expect(spyCaptureException).toBeCalledTimes(1); expect(spyCaptureException).toHaveBeenCalledWith(error, { level: 'error', - extra: { developerMessage: errorMessage }, + extra: { + developerMessage: errorMessage, + message: '', + originalError: error.stack, + }, + }); + }); + + /** + * Test Log level map with capture message for string error + */ + it('LoggerSentryTransport.correctValues string error', async () => { + const logLevel = 'log' as LogLevel; + const error = 'Test error'; + loggerSentryTransport.log(errorMessage, logLevel, error); + + expect(spyLogLevelMap).toBeCalledTimes(1); + expect(spyLogLevelMap).toHaveBeenCalledWith(logLevel); + expect(spyCaptureException).toBeCalledTimes(1); + expect(spyCaptureException).toHaveBeenCalledWith(error, { + level: 'error', + extra: { + developerMessage: errorMessage, + message: error, + }, + }); + }); + + /** + * Test Log level map with capture message for BadRequestException + */ + it('LoggerSentryTransport.correctValues BadRequestException', async () => { + const logLevel = 'log' as LogLevel; + const error = new BadRequestException(); + const res = error.getResponse(); + loggerSentryTransport.log(errorMessage, logLevel, error); + + expect(spyLogLevelMap).toBeCalledTimes(1); + expect(spyLogLevelMap).toHaveBeenCalledWith(logLevel); + expect(spyCaptureException).toBeCalledTimes(1); + const statusCode = error.getStatus(); + expect(spyCaptureException).toHaveBeenCalledWith(error, { + level: 'error', + extra: { + developerMessage: errorMessage, + statusCode, + errorCode: mapHttpStatus(statusCode as number), + message: isObject(res) && 'message' in res ? res.message : res, + }, + }); + }); + + /** + * Test Log level map with capture exception + */ + it('LoggerSentryTransport.correctValues Exception', async () => { + class TestException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'Test Exception', + httpStatus: HttpStatus.BAD_REQUEST, + ...options, + }); + this.errorCode = 'INVALID_LOGIN_DATA_ERROR'; + } + } + const logLevel = 'log' as LogLevel; + const exception = new TestException(); + + loggerSentryTransport.log(errorMessage, logLevel, exception); + + expect(spyLogLevelMap).toBeCalledTimes(1); + expect(spyLogLevelMap).toHaveBeenCalledWith(logLevel); + expect(spyCaptureException).toBeCalledTimes(1); + expect(spyCaptureException).toHaveBeenCalledWith(exception, { + level: 'error', + extra: { + developerMessage: errorMessage, + errorCode: exception?.errorCode, + statusCode: exception?.httpStatus, + message: exception?.message, + safeMessage: exception?.safeMessage, + originalError: exception?.context?.originalError, + context: exception?.context, + + }, }); }); diff --git a/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.ts b/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.ts index da66285f0..5227162fc 100644 --- a/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.ts +++ b/packages/nestjs-logger-sentry/src/transports/logger-sentry.transport.ts @@ -1,8 +1,11 @@ -import { Inject, Injectable, LogLevel } from '@nestjs/common'; +import { HttpException, Inject, Injectable, LogLevel } from '@nestjs/common'; import * as Sentry from '@sentry/node'; import { LoggerTransportInterface } from '@concepta/nestjs-logger'; import { LoggerSentrySettingsInterface } from '../interfaces/logger-sentry-settings.interface'; import { LOGGER_SENTRY_MODULE_SETTINGS_TOKEN } from '../config/logger-sentry.config'; +import { RuntimeException, mapHttpStatus } from '@concepta/nestjs-exception'; +import { isObject } from 'class-validator'; +import { LoggerSentryExtrasInterface } from '../interfaces/logger-sentry-extras.interface'; /** * The transport that implements {@link LoggerTransportInterface} @@ -46,7 +49,11 @@ export class LoggerSentryTransport implements LoggerTransportInterface { * @param logLevel - Level of severity * @param error - Error to log */ - log(message: string, logLevel: LogLevel, error?: Error | string): void { + log( + message: string, + logLevel: LogLevel, + error?: Error | string | RuntimeException, + ): void { // map the internal log level to sentry log severity const severity = this.settings.logLevelMap(logLevel); @@ -55,12 +62,68 @@ export class LoggerSentryTransport implements LoggerTransportInterface { // its an error, use error message Sentry.captureException(error, { level: severity, - // TODO: are we using this extras correctly? - extra: { developerMessage: message }, + extra: { + developerMessage: message, + ...this.getExtras(error), + }, }); } else { // its a string, just send it Sentry.captureMessage(message, severity); } } + + private getExtras( + exception?: Error | string | RuntimeException | HttpException, + ): LoggerSentryExtrasInterface { + const extras: LoggerSentryExtrasInterface = {}; + if (exception instanceof HttpException) { + this.handleHttpException(exception, extras); + } else if (exception instanceof RuntimeException) { + this.handleRuntimeException(exception, extras); + } else if (exception instanceof Error) { + this.handleError(exception, extras); + } else if (typeof exception === 'string') { + this.handleStringException(exception, extras); + } + + return extras; + } + + private handleHttpException( + exception: HttpException, + extras: LoggerSentryExtrasInterface, + ): void { + const res = exception.getResponse(); + extras.statusCode = exception.getStatus(); + extras.errorCode = mapHttpStatus(extras.statusCode); + extras.message = isObject(res) && 'message' in res ? res.message : res; + } + + private handleRuntimeException( + exception: RuntimeException, + extras: LoggerSentryExtrasInterface, + ): void { + extras.errorCode = exception?.errorCode; + extras.statusCode = exception?.httpStatus; + extras.message = exception?.message; + extras.safeMessage = exception?.safeMessage; + extras.originalError = exception?.context?.originalError; + extras.context = exception?.context; + } + + private handleError( + exception: Error, + extras: LoggerSentryExtrasInterface, + ): void { + extras.message = exception?.message; + extras.originalError = exception?.stack || ''; + } + + private handleStringException( + exception: string, + extras: LoggerSentryExtrasInterface, + ): void { + extras.message = exception; + } } diff --git a/packages/nestjs-org/package.json b/packages/nestjs-org/package.json index c74893be7..c615c0747 100644 --- a/packages/nestjs-org/package.json +++ b/packages/nestjs-org/package.json @@ -16,6 +16,7 @@ "@concepta/nestjs-common": "^5.0.0-alpha.4", "@concepta/nestjs-crud": "^5.0.0-alpha.4", "@concepta/nestjs-event": "^5.0.0-alpha.4", + "@concepta/nestjs-exception": "^5.0.0-alpha.4", "@concepta/nestjs-typeorm-ext": "^5.0.0-alpha.4", "@concepta/ts-common": "^5.0.0-alpha.4", "@concepta/ts-core": "^5.0.0-alpha.4", diff --git a/packages/nestjs-org/src/exceptions/org-member.exception.ts b/packages/nestjs-org/src/exceptions/org-member.exception.ts index 55f9bbf0a..d3effe37a 100644 --- a/packages/nestjs-org/src/exceptions/org-member.exception.ts +++ b/packages/nestjs-org/src/exceptions/org-member.exception.ts @@ -1,9 +1,17 @@ -import { ExceptionInterface } from '@concepta/ts-core'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; -export class OrgMemberException extends Error implements ExceptionInterface { - errorCode = 'ORG_MEMBER_ERROR'; - - constructor(message: string) { - super(message); +export class OrgMemberException extends RuntimeException { + constructor( + message: string, + options?: Omit, + ) { + super({ + message, + ...options, + }); + this.errorCode = 'ORG_MEMBER_ERROR'; } } diff --git a/packages/nestjs-org/src/exceptions/org-not-found.exception.ts b/packages/nestjs-org/src/exceptions/org-not-found.exception.ts index 5687efb71..9c7f8e747 100644 --- a/packages/nestjs-org/src/exceptions/org-not-found.exception.ts +++ b/packages/nestjs-org/src/exceptions/org-not-found.exception.ts @@ -1,9 +1,14 @@ -import { ExceptionInterface } from '@concepta/ts-core'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; -export class OrgNotFoundException extends Error implements ExceptionInterface { - errorCode = 'ORG_NOT_FOUND_ERROR'; - - constructor(message = 'The org was not found') { - super(message); +export class OrgNotFoundException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'The org was not found', + ...options, + }); + this.errorCode = 'ORG_NOT_FOUND_ERROR'; } } diff --git a/packages/nestjs-password/package.json b/packages/nestjs-password/package.json index 959fd640b..729eae8e0 100644 --- a/packages/nestjs-password/package.json +++ b/packages/nestjs-password/package.json @@ -13,6 +13,7 @@ ], "dependencies": { "@concepta/nestjs-common": "^5.0.0-alpha.4", + "@concepta/nestjs-exception": "^5.0.0-alpha.4", "@concepta/ts-common": "^5.0.0-alpha.4", "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", diff --git a/packages/nestjs-password/src/exceptions/current-password-required.exception.ts b/packages/nestjs-password/src/exceptions/current-password-required.exception.ts new file mode 100644 index 000000000..b12e0649e --- /dev/null +++ b/packages/nestjs-password/src/exceptions/current-password-required.exception.ts @@ -0,0 +1,17 @@ +import { HttpStatus } from '@nestjs/common'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; + +export class CurrentPasswordRequiredException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'Current password is required', + httpStatus: HttpStatus.BAD_REQUEST, + ...options, + }); + + this.errorCode = 'PASSWORD_CURRENT_REQUIRED_ERROR'; + } +} diff --git a/packages/nestjs-password/src/exceptions/password-not-strong.exception.ts b/packages/nestjs-password/src/exceptions/password-not-strong.exception.ts new file mode 100644 index 000000000..91d544191 --- /dev/null +++ b/packages/nestjs-password/src/exceptions/password-not-strong.exception.ts @@ -0,0 +1,17 @@ +import { HttpStatus } from '@nestjs/common'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; + +export class PasswordNotStrongException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'Password is not strong enough', + httpStatus: HttpStatus.BAD_REQUEST, + ...options, + }); + + this.errorCode = 'PASSWORD_NOT_STRONG_ERROR'; + } +} diff --git a/packages/nestjs-password/src/exceptions/password-used-recently.exception.ts b/packages/nestjs-password/src/exceptions/password-used-recently.exception.ts new file mode 100644 index 000000000..b45e4bb90 --- /dev/null +++ b/packages/nestjs-password/src/exceptions/password-used-recently.exception.ts @@ -0,0 +1,18 @@ +import { HttpStatus } from '@nestjs/common'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; + +export class PasswordUsedRecentlyException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: + 'The new password has been used too recently, please use a different password', + httpStatus: HttpStatus.BAD_REQUEST, + ...options, + }); + + this.errorCode = 'PASSWORD_USED_RECENTLY_ERROR'; + } +} diff --git a/packages/nestjs-password/src/services/password-creation.service.spec.ts b/packages/nestjs-password/src/services/password-creation.service.spec.ts index 937f55a0b..5e79a8bc3 100644 --- a/packages/nestjs-password/src/services/password-creation.service.spec.ts +++ b/packages/nestjs-password/src/services/password-creation.service.spec.ts @@ -7,6 +7,7 @@ import { PasswordCreationService } from './password-creation.service'; import { PasswordStorageService } from './password-storage.service'; import { PasswordStrengthService } from './password-strength.service'; import { PasswordValidationService } from './password-validation.service'; +import { PasswordNotStrongException } from '../exceptions/password-not-strong.exception'; describe(PasswordCreationService, () => { let config: PasswordSettingsInterface; @@ -75,7 +76,7 @@ describe(PasswordCreationService, () => { }); }; - await expect(t).rejects.toThrow(Error); + await expect(t).rejects.toThrow(PasswordNotStrongException); await expect(t).rejects.toThrow('Password is not strong enough'); }); }); diff --git a/packages/nestjs-password/src/services/password-creation.service.ts b/packages/nestjs-password/src/services/password-creation.service.ts index 880760394..6cda3abd2 100644 --- a/packages/nestjs-password/src/services/password-creation.service.ts +++ b/packages/nestjs-password/src/services/password-creation.service.ts @@ -11,6 +11,9 @@ import { PasswordValidationService } from './password-validation.service'; import { PasswordCreateObjectOptionsInterface } from '../interfaces/password-create-object-options.interface'; import { PasswordCurrentPasswordInterface } from '../interfaces/password-current-password.interface'; import { PasswordHistoryPasswordInterface } from '../interfaces/password-history-password.interface'; +import { PasswordNotStrongException } from '../exceptions/password-not-strong.exception'; +import { CurrentPasswordRequiredException } from '../exceptions/current-password-required.exception'; +import { PasswordUsedRecentlyException } from '../exceptions/password-used-recently.exception'; /** * Service with functions related to password creation @@ -78,7 +81,7 @@ export class PasswordCreationService if (typeof password === 'string') { // check strength if (!this.passwordStrengthService.isStrong(password)) { - throw new Error('Password is not strong enough'); + throw new PasswordNotStrongException(); } } @@ -103,7 +106,7 @@ export class PasswordCreationService if (this.settings?.requireCurrentToUpdate === true) { // TODO: should be a password exception class // reqs not met, throw exception - throw new Error('Current password is required'); + throw new CurrentPasswordRequiredException(); } } @@ -135,9 +138,7 @@ export class PasswordCreationService // is valid? if (isValid) { - throw new Error( - 'The new password has been used too recently, please use a different password', - ); + throw new PasswordUsedRecentlyException(); } } } diff --git a/packages/nestjs-user/package.json b/packages/nestjs-user/package.json index 851fef155..91bad23bb 100644 --- a/packages/nestjs-user/package.json +++ b/packages/nestjs-user/package.json @@ -16,6 +16,7 @@ "@concepta/nestjs-common": "^5.0.0-alpha.4", "@concepta/nestjs-crud": "^5.0.0-alpha.4", "@concepta/nestjs-event": "^5.0.0-alpha.4", + "@concepta/nestjs-exception": "^5.0.0-alpha.4", "@concepta/nestjs-password": "^5.0.0-alpha.4", "@concepta/nestjs-typeorm-ext": "^5.0.0-alpha.4", "@concepta/ts-common": "^5.0.0-alpha.4", diff --git a/packages/nestjs-user/src/exceptions/user-exception.ts b/packages/nestjs-user/src/exceptions/user-exception.ts index a533a6552..b10324373 100644 --- a/packages/nestjs-user/src/exceptions/user-exception.ts +++ b/packages/nestjs-user/src/exceptions/user-exception.ts @@ -1,21 +1,14 @@ -import { ExceptionInterface, mapNonErrorToException } from '@concepta/ts-core'; - +import { mapNonErrorToException } from '@concepta/ts-core'; +import { RuntimeException } from '@concepta/nestjs-exception'; /** * Generic user exception. */ -export class UserException extends Error implements ExceptionInterface { - errorCode = 'USER_ERROR'; - - context: { - message: string; - originalError: Error; - }; - +export class UserException extends RuntimeException { constructor(message: string, originalError?: unknown) { - super(message); - this.context = { + super({ message, originalError: mapNonErrorToException(originalError), - }; + }); + this.errorCode = 'USER_ERROR'; } } diff --git a/packages/nestjs-user/src/exceptions/user-not-found-exception.ts b/packages/nestjs-user/src/exceptions/user-not-found-exception.ts index 5f6b9a789..c857d2300 100644 --- a/packages/nestjs-user/src/exceptions/user-not-found-exception.ts +++ b/packages/nestjs-user/src/exceptions/user-not-found-exception.ts @@ -1,9 +1,16 @@ -import { ExceptionInterface } from '@concepta/ts-core'; +import { + RuntimeException, + RuntimeExceptionOptions, +} from '@concepta/nestjs-exception'; +import { HttpStatus } from '@nestjs/common'; -export class UserNotFoundException extends Error implements ExceptionInterface { - errorCode = 'USER_NOT_FOUND_ERROR'; - - constructor(message = 'The user was not found') { - super(message); +export class UserNotFoundException extends RuntimeException { + constructor(options?: RuntimeExceptionOptions) { + super({ + message: 'The user was not found', + httpStatus: HttpStatus.NOT_FOUND, + ...options, + }); + this.errorCode = 'USER_NOT_FOUND_ERROR'; } } diff --git a/packages/nestjs-user/src/services/user-password.service.ts b/packages/nestjs-user/src/services/user-password.service.ts index b90c7a050..d143c8a1c 100644 --- a/packages/nestjs-user/src/services/user-password.service.ts +++ b/packages/nestjs-user/src/services/user-password.service.ts @@ -132,9 +132,9 @@ export class UserPasswordService implements UserPasswordServiceInterface { } // throw an exception by default - throw new UserNotFoundException( - 'Impossible to update password if user is not found', - ); + throw new UserNotFoundException({ + message: 'Impossible to update password if user is not found', + }); } protected async validateCurrent( diff --git a/yarn.lock b/yarn.lock index c3aba12c8..0d6b9ae93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -816,6 +816,7 @@ __metadata: "@concepta/nestjs-auth-jwt": "npm:^5.0.0-alpha.4" "@concepta/nestjs-authentication": "npm:^5.0.0-alpha.4" "@concepta/nestjs-common": "npm:^5.0.0-alpha.4" + "@concepta/nestjs-exception": "npm:^5.0.0-alpha.4" "@concepta/nestjs-jwt": "npm:^5.0.0-alpha.4" "@concepta/nestjs-password": "npm:^5.0.0-alpha.4" "@concepta/ts-common": "npm:^5.0.0-alpha.4" @@ -1177,6 +1178,7 @@ __metadata: resolution: "@concepta/nestjs-logger-sentry@workspace:packages/nestjs-logger-sentry" dependencies: "@concepta/nestjs-common": "npm:^5.0.0-alpha.4" + "@concepta/nestjs-exception": "npm:^5.0.0-alpha.4" "@concepta/nestjs-logger": "npm:^5.0.0-alpha.4" "@nestjs/common": "npm:^10.4.1" "@nestjs/config": "npm:^3.2.3" @@ -1218,6 +1220,7 @@ __metadata: "@concepta/nestjs-common": "npm:^5.0.0-alpha.4" "@concepta/nestjs-crud": "npm:^5.0.0-alpha.4" "@concepta/nestjs-event": "npm:^5.0.0-alpha.4" + "@concepta/nestjs-exception": "npm:^5.0.0-alpha.4" "@concepta/nestjs-invitation": "npm:^5.0.0-alpha.4" "@concepta/nestjs-logger": "npm:^5.0.0-alpha.4" "@concepta/nestjs-password": "npm:^5.0.0-alpha.4" @@ -1269,6 +1272,7 @@ __metadata: resolution: "@concepta/nestjs-password@workspace:packages/nestjs-password" dependencies: "@concepta/nestjs-common": "npm:^5.0.0-alpha.4" + "@concepta/nestjs-exception": "npm:^5.0.0-alpha.4" "@concepta/ts-common": "npm:^5.0.0-alpha.4" "@nestjs/common": "npm:^10.4.1" "@nestjs/config": "npm:^3.2.3" @@ -1406,6 +1410,7 @@ __metadata: "@concepta/nestjs-common": "npm:^5.0.0-alpha.4" "@concepta/nestjs-crud": "npm:^5.0.0-alpha.4" "@concepta/nestjs-event": "npm:^5.0.0-alpha.4" + "@concepta/nestjs-exception": "npm:^5.0.0-alpha.4" "@concepta/nestjs-jwt": "npm:^5.0.0-alpha.4" "@concepta/nestjs-password": "npm:^5.0.0-alpha.4" "@concepta/nestjs-typeorm-ext": "npm:^5.0.0-alpha.4"