diff --git a/.gitignore b/.gitignore index e6a8984..795d7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ infra/postgres .next out .yarn/install-state.gz +.yarn +.yarn/install-state.gz diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz deleted file mode 100644 index 83fcad1..0000000 Binary files a/.yarn/install-state.gz and /dev/null differ diff --git a/apps/api/src/app/app.config.ts b/apps/api/src/app/app.config.ts index 91e8456..9c6eaaa 100644 --- a/apps/api/src/app/app.config.ts +++ b/apps/api/src/app/app.config.ts @@ -4,7 +4,14 @@ import { ApplicationBootstrapOptions } from '../bootstrap'; import 'dotenv/config'; export default (): ApplicationBootstrapOptions => ({ - port: parseInt(process.env[CONFIG_APP.SERVER_PORT]) || 3000, + port: parseInt(process.env[CONFIG_APP.SERVER_PORT]!) || 3000, + security: { + expiresIn: process.env[CONFIG_APP.SECURITY_EXPIRES_IN]!, + refreshIn: process.env[CONFIG_APP.SECURITY_REFRESH_IN]!, + bcryptSalt: process.env[CONFIG_APP.SECURITY_BCRYPT_SALT]!, + jwtRefreshSecret: process.env[CONFIG_APP.JWT_ACCESS_SECRET]!, + jwtAccessSecret: process.env[CONFIG_APP.JWT_REFRESH_SECRET]!, + }, mail: { provider: process.env[CONFIG_APP.MAIL_INFRA] as MailInfraProvider, options: { @@ -20,10 +27,10 @@ export default (): ApplicationBootstrapOptions => ({ } as MailInfraProviderOptions, }, database: { - host: process.env[CONFIG_APP.DB_HOST], - port: Number.parseInt(process.env[CONFIG_APP.DB_PORT]), - database: process.env[CONFIG_APP.DB_DATABASE], - user: process.env[CONFIG_APP.DB_USER], - password: process.env[CONFIG_APP.DB_PASSWORD], + host: process.env[CONFIG_APP.DB_HOST]!, + port: Number.parseInt(process.env[CONFIG_APP.DB_PORT]!), + database: process.env[CONFIG_APP.DB_DATABASE]!, + user: process.env[CONFIG_APP.DB_USER]!, + password: process.env[CONFIG_APP.DB_PASSWORD]!, }, }); diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 067fc78..dce0aca 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,33 +1,73 @@ -import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { configSchema } from '@goran/config'; import { ApplicationBootstrapOptions } from '../bootstrap'; import { UsersModule } from '@goran/users'; -import { AuthenticationModule } from '@goran/security'; +import { + AuthenticationModule, + PasswordResetModule, + SessionsModule, + TokensModule, + PasswordModule, +} from '@goran/security'; import { DatabaseModule } from '@goran/drizzle-data-access'; import { MailModule } from '@goran/mail'; +import { DeviceDetectorModule } from '@goran/device-detector'; +import { IpLocatorModule } from '@goran/ip-locator'; +import { CacheModule } from '@nestjs/cache-manager'; +import { JwtModule } from '@nestjs/jwt'; +import { CqrsModule } from '@nestjs/cqrs'; +import { LoggerModule } from 'nestjs-pino'; +import { PassportModule } from '@nestjs/passport'; -@Module({ - imports: [ConfigModule], - controllers: [AppController], - providers: [AppService], -}) export class AppModule { static register(options: ApplicationBootstrapOptions) { return { module: AppModule, + controllers: [AppController], + providers: [AppService], imports: [ + LoggerModule.forRoot({ + pinoHttp: { + customProps: (req, res) => ({ + context: 'HTTP', + }), + transport: { + target: 'pino-pretty', + options: { + singleLine: true, + }, + }, + }, + }), + CqrsModule.forRoot(), ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env', '.env.local'], validationSchema: configSchema, }), - UsersModule, - AuthenticationModule, + CacheModule.register({ isGlobal: true }), DatabaseModule.forRoot(options.database), MailModule.register(options.mail), + JwtModule.register({ + global: true, + secret: options.security.jwtAccessSecret, + signOptions: { + expiresIn: options.security.expiresIn, + }, + }), + PassportModule.register({ global: true }), + UsersModule, + PasswordModule, + TokensModule, + IpLocatorModule, + DeviceDetectorModule, + SessionsModule.register({ + refreshIn: options.security.refreshIn, + }), + AuthenticationModule, + PasswordResetModule, ], }; } diff --git a/apps/api/src/bootstrap/application-bootstrap-options.interface.ts b/apps/api/src/bootstrap/application-bootstrap-options.interface.ts index 246d76a..d42733a 100644 --- a/apps/api/src/bootstrap/application-bootstrap-options.interface.ts +++ b/apps/api/src/bootstrap/application-bootstrap-options.interface.ts @@ -1,8 +1,10 @@ import { DatabaseOptions } from '@goran/drizzle-data-access'; import { MailOptions } from '@goran/mail'; +import { SecurityOptions } from '@goran/security'; export interface ApplicationBootstrapOptions { port: number; + security: SecurityOptions; mail: MailOptions; database: DatabaseOptions; } diff --git a/apps/api/src/bootstrap/globals.ts b/apps/api/src/bootstrap/globals.ts index e2a88f1..2cd57a4 100644 --- a/apps/api/src/bootstrap/globals.ts +++ b/apps/api/src/bootstrap/globals.ts @@ -5,6 +5,7 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import cookieParser from 'cookie-parser'; +import { Logger, LoggerErrorInterceptor } from 'nestjs-pino'; /** * Registers global pipes and interceptors, plus server conifguration @@ -12,15 +13,17 @@ import cookieParser from 'cookie-parser'; * @param app - Nestjs application object * @returns Modified and configured nestjs application object and certain parameters */ -export async function registerGlobals(app: INestApplication) { - const globalPrefix = 'api'; +export async function registerGlobals( + app: INestApplication, + globalPrefix: string +) { app.enableCors(); app.setGlobalPrefix(globalPrefix); app.use(cookieParser()); app.useGlobalPipes(new ValidationPipe()); + app.useLogger(app.get(Logger)); + app.useGlobalInterceptors(new LoggerErrorInterceptor()); app.useGlobalInterceptors( new ClassSerializerInterceptor(app.get(Reflector)) ); - - return { app, globalPrefix }; } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index db9cbca..c671890 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -8,23 +8,24 @@ import appConfig from './app/app.config'; async function bootstrap() { const logger = new Logger('BOOTSTRAP'); - - logger.log(goranBanner); - - const config = appConfig(); - const app = await NestFactory.create(AppModule.register(config)); + const globalPrefix = 'api'; const port = process.env[CONFIG_APP.SERVER_PORT] || 3000; - const { globalPrefix } = await registerGlobals(app); - - setupSwagger(app); + const config = appConfig(); logger.log( - ` Application is running on: http://localhost:${config.port}/${globalPrefix}` - ); - logger.log( - ` Documentation is running on: http://localhost:${config.port}/docs` + goranBanner({ + appLink: `http://localhost:${config.port}/${globalPrefix}`, + docsLink: `http://localhost:${config.port}/docs`, + }) ); + const app = await NestFactory.create(AppModule.register(config), { + bufferLogs: true, + }); + await registerGlobals(app, globalPrefix); + + setupSwagger(app); + await app.listen(port); } diff --git a/libs/core/config/.eslintrc.json b/libs/core/config/.eslintrc.json deleted file mode 100644 index 3456be9..0000000 --- a/libs/core/config/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/core/config/project.json b/libs/core/config/project.json deleted file mode 100644 index f056695..0000000 --- a/libs/core/config/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "config", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/core/config/src", - "projectType": "library", - "tags": [], - "// targets": "to see all targets run: nx show project config --web", - "targets": {} -} diff --git a/libs/core/config/tsconfig.json b/libs/core/config/tsconfig.json deleted file mode 100644 index f2400ab..0000000 --- a/libs/core/config/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] -} diff --git a/libs/core/config/tsconfig.lib.json b/libs/core/config/tsconfig.lib.json deleted file mode 100644 index 8f9c818..0000000 --- a/libs/core/config/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/core/mail/.eslintrc.json b/libs/core/mail/.eslintrc.json deleted file mode 100644 index 3456be9..0000000 --- a/libs/core/mail/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/core/mail/jest.config.ts b/libs/core/mail/jest.config.ts deleted file mode 100644 index d56a947..0000000 --- a/libs/core/mail/jest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'mail', - preset: '../../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../../coverage/libs/core/mail', - passWithNoTests: true, -}; diff --git a/libs/core/mail/project.json b/libs/core/mail/project.json deleted file mode 100644 index c7b92ce..0000000 --- a/libs/core/mail/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "mail", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/core/mail/src", - "projectType": "library", - "tags": [], - "// targets": "to see all targets run: nx show project mail --web", - "targets": {} -} diff --git a/libs/core/mail/tsconfig.json b/libs/core/mail/tsconfig.json deleted file mode 100644 index 8122543..0000000 --- a/libs/core/mail/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/core/mail/tsconfig.lib.json b/libs/core/mail/tsconfig.lib.json deleted file mode 100644 index dbf54fd..0000000 --- a/libs/core/mail/tsconfig.lib.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"], - "target": "es2021", - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/core/mail/tsconfig.spec.json b/libs/core/mail/tsconfig.spec.json deleted file mode 100644 index 69a251f..0000000 --- a/libs/core/mail/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/libs/core/security/README.md b/libs/core/security/README.md index 9930083..066a3de 100644 --- a/libs/core/security/README.md +++ b/libs/core/security/README.md @@ -1,6 +1,9 @@ -# security +# @goran/security -This library was generated with [Nx](https://nx.dev). +- Session Management +- SecurityMetrics +- Password Reset +- Authentication ## Running unit tests diff --git a/libs/core/security/src/lib/authentication/application/commands/index.ts b/libs/core/security/src/lib/authentication/application/commands/index.ts index f6b076f..21b41e2 100644 --- a/libs/core/security/src/lib/authentication/application/commands/index.ts +++ b/libs/core/security/src/lib/authentication/application/commands/index.ts @@ -1,5 +1,3 @@ -export * from './request-password-reset'; -export * from './verify-password-reset-attempt'; -export * from './reset-password'; -export * from './signup'; -export * from './signin'; +export * from './sign-up'; +export * from './sign-in'; +export * from './sign-out'; diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-in/index.ts b/libs/core/security/src/lib/authentication/application/commands/sign-in/index.ts new file mode 100644 index 0000000..0e45822 --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-in/index.ts @@ -0,0 +1,2 @@ +export * from './sign-in.command-handler'; +export * from './sign-in.command'; diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-in/sign-in.command-handler.ts b/libs/core/security/src/lib/authentication/application/commands/sign-in/sign-in.command-handler.ts new file mode 100644 index 0000000..a9403e9 --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-in/sign-in.command-handler.ts @@ -0,0 +1,68 @@ +import { Logger } from '@nestjs/common'; +import { SignInCommand } from './sign-in.command'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { SessionsService } from '../../../../sessions'; +import { PasswordService } from '../../../../password'; +import { UserMapper, UsersService } from '@goran/users'; +import { Result, Err, Ok } from 'oxide.ts'; +import { InvalidAuthenticationCredentials } from '../../../domain'; +import { AuthenticationCredentialDto } from '../../dtos'; +import { ExceptionBase, Guard } from '@goran/common'; +import { IpLocatorService } from '@goran/ip-locator'; +import { DeviceDetectorService } from '@goran/device-detector'; + +@CommandHandler(SignInCommand) +export class SignInCommandHandler implements ICommandHandler { + private readonly logger = new Logger(SignInCommand.name); + + constructor( + private readonly passwordService: PasswordService, + private readonly ipLocator: IpLocatorService, + private readonly sessionsService: SessionsService, + private readonly deviceDetector: DeviceDetectorService, + private readonly userMapper: UserMapper, + private usersService: UsersService + ) {} + + async execute( + command: SignInCommand + ): Promise> { + const { username, email, password } = command; + const userResult = await this.usersService.findUserByIdenfitier({ + username, + email, + }); + + if (userResult.isErr()) { + return Err(new InvalidAuthenticationCredentials()); + } + const userDto = userResult.unwrap(); + const passwordIsValid = await this.passwordService.comparePassword( + password, + userDto.password + ); + const user = await this.userMapper.toDomain(userDto); + if (!passwordIsValid) { + return Err(new InvalidAuthenticationCredentials()); + } + + const sessionCreationResult = await this.sessionsService.createSession( + user, + command.clientInfo.ip ?? '', + await this.ipLocator.getLocation(command.clientInfo.ip ?? ''), + !Guard.isEmpty(command.clientInfo.userAgent) + ? this.deviceDetector.getDevice( + command.clientInfo.userAgent ?? '' + ) + : undefined + ); + + const [tokens, session] = sessionCreationResult.unwrap(); + + this.logger.verbose( + `User with ${userDto.email} email is authenticated` + ); + + return Ok(new AuthenticationCredentialDto({ userId: user.id, tokens })); + } +} diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-in/sign-in.command.ts b/libs/core/security/src/lib/authentication/application/commands/sign-in/sign-in.command.ts new file mode 100644 index 0000000..a816c0e --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-in/sign-in.command.ts @@ -0,0 +1,24 @@ +import { Command, CommandProps, Optional } from '@goran/common'; +import { ClientInfoDto } from '../../dtos'; + +/** + * Signup - User Registration + * + * @param fullname + * @param username + * @param password + * @param email + */ +export class SignInCommand extends Command { + readonly email: Optional; + readonly username: Optional; + readonly password: string; + readonly clientInfo: ClientInfoDto; + + constructor(props: CommandProps) { + super(props); + this.username = props.username; + this.email = props.email; + this.password = props.password; + } +} diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-out/index.ts b/libs/core/security/src/lib/authentication/application/commands/sign-out/index.ts new file mode 100644 index 0000000..e2b2f34 --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-out/index.ts @@ -0,0 +1,2 @@ +export * from './sign-out.command-handler'; +export * from './sign-out.command'; diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-out/sign-out.command-handler.ts b/libs/core/security/src/lib/authentication/application/commands/sign-out/sign-out.command-handler.ts new file mode 100644 index 0000000..c3a53e9 --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-out/sign-out.command-handler.ts @@ -0,0 +1,33 @@ +import { Logger } from '@nestjs/common'; +import { SignOutCommand } from './sign-out.command'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { SessionsService } from '../../../../sessions'; +import { Result, Ok } from 'oxide.ts'; +import { ExceptionBase } from '@goran/common'; + +@CommandHandler(SignOutCommand) +export class SignOutCommandHandler implements ICommandHandler { + private readonly logger = new Logger(SignOutCommand.name); + + constructor(private readonly sessionsService: SessionsService) {} + + async execute( + command: SignOutCommand + ): Promise> { + const { refreshToken } = command; + + const sessionRevokeResult = + await this.sessionsService.revokeSessionByRefreshToken( + refreshToken + ); + + if(sessionRevokeResult.isErr()) + return sessionRevokeResult; + + await this.sessionsService.deleteSession( + sessionRevokeResult.unwrap().getProps().refreshToken + ); + + return Ok(true); + } +} diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-out/sign-out.command.ts b/libs/core/security/src/lib/authentication/application/commands/sign-out/sign-out.command.ts new file mode 100644 index 0000000..457f2b4 --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-out/sign-out.command.ts @@ -0,0 +1,18 @@ +import { Command, CommandProps } from '@goran/common'; + +/** + * Sign Out - Revokes the user session permenantly + * + * @param userId + * @param refreshToken + */ +export class SignOutCommand extends Command { + readonly userId: string; + readonly refreshToken: string; + + constructor(props: CommandProps) { + super(props); + this.userId = props.userId; + this.refreshToken = props.refreshToken; + } +} diff --git a/libs/core/security/src/lib/authentication/application/commands/signup/index.ts b/libs/core/security/src/lib/authentication/application/commands/sign-up/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/signup/index.ts rename to libs/core/security/src/lib/authentication/application/commands/sign-up/index.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-up/signup.command-handler.ts b/libs/core/security/src/lib/authentication/application/commands/sign-up/signup.command-handler.ts new file mode 100644 index 0000000..22d4fbd --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-up/signup.command-handler.ts @@ -0,0 +1,58 @@ +import { Logger } from '@nestjs/common'; +import { SignUpCommand } from './signup.command'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { UsersService } from '@goran/users'; +import { Ok, Result } from 'oxide.ts'; +import { ExceptionBase, Guard } from '@goran/common'; +import { AuthenticationCredentialDto } from '../../dtos'; +import { SessionsService } from '../../../../sessions'; +import { PasswordService } from '../../../../password'; +import { IpLocatorService } from '@goran/ip-locator'; +import { DeviceDetectorService } from '@goran/device-detector'; + +@CommandHandler(SignUpCommand) +export class SignUpCommandHandler implements ICommandHandler { + constructor( + private readonly sessionsService: SessionsService, + private readonly passwordService: PasswordService, + private readonly usersService: UsersService, + private readonly ipLocator: IpLocatorService, + private readonly deviceDetector: DeviceDetectorService + ) {} + private readonly logger = new Logger(SignUpCommand.name); + + async execute( + command: SignUpCommand + ): Promise> { + const hashedPassword = await this.passwordService.hashPassword( + command.password + ); + const userResult = await this.usersService.create({ + ...command, + password: hashedPassword, + }); + + if (userResult.isErr()) { + return userResult; + } + + const user = userResult.unwrap(); + + this.logger.verbose(`User with ${user.email} email signed up`); + + const sessionCreationResult = await this.sessionsService.createSession( + user, + command.clientInfo.ip ?? '', + await this.ipLocator.getLocation(command.clientInfo.ip ?? ''), + !Guard.isEmpty(command.clientInfo.userAgent) + ? this.deviceDetector.getDevice( + command.clientInfo.userAgent ?? '' + ) + : undefined + ); + + const [tokens, session] = sessionCreationResult.unwrap(); + + return Ok(new AuthenticationCredentialDto({ userId: user.id, tokens })); + } +} diff --git a/libs/core/security/src/lib/authentication/application/commands/sign-up/signup.command.ts b/libs/core/security/src/lib/authentication/application/commands/sign-up/signup.command.ts new file mode 100644 index 0000000..49cb761 --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/commands/sign-up/signup.command.ts @@ -0,0 +1,28 @@ +import { Command, CommandProps, Optional } from '@goran/common'; +import { ClientInfoDto } from '../../dtos'; + +/** + * Signup - User Registration + * + * @param {Optional} fullname + * @param {string} username + * @param {string} password + * @param {string} email + * @param {ClientInfoDto} clientInfo @readonly + */ +export class SignUpCommand extends Command { + readonly email: string; + readonly fullname: Optional; + readonly username: string; + readonly password: string; + readonly clientInfo: ClientInfoDto; + + constructor(props: CommandProps) { + super(props); + this.fullname = props.fullname; + this.username = props.username; + this.email = props.email; + this.password = props.password; + this.clientInfo = props.clientInfo; + } +} diff --git a/libs/core/security/src/lib/authentication/application/commands/signin/index.ts b/libs/core/security/src/lib/authentication/application/commands/signin/index.ts deleted file mode 100644 index 9b9ed56..0000000 --- a/libs/core/security/src/lib/authentication/application/commands/signin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './signin.command-handler'; -export * from './signin.command'; diff --git a/libs/core/security/src/lib/authentication/application/commands/signin/signin.command-handler.ts b/libs/core/security/src/lib/authentication/application/commands/signin/signin.command-handler.ts deleted file mode 100644 index 4ddb0cd..0000000 --- a/libs/core/security/src/lib/authentication/application/commands/signin/signin.command-handler.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { SigninCommand } from './signin.command'; -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { PasswordService } from '../../services'; -import { UsersService } from '@goran/users'; -import { Result, Err, Ok } from 'oxide.ts'; -import { InvalidAuthenticationCredentials } from '../../../domain'; -import { AuthenticationCredentialDto } from '../../dtos'; -import { ExceptionBase } from '@goran/common'; -import { AuthenticationTokenFactory } from '../../factories'; - -@CommandHandler(SigninCommand) -export class SigninCommandHandler implements ICommandHandler { - private readonly logger = new Logger(SigninCommand.name); - - constructor( - private tokenFactory: AuthenticationTokenFactory, - private passwordService: PasswordService, - private usersService: UsersService - ) {} - - async execute( - command: SigninCommand - ): Promise> { - const { username, email, password } = command; - const userResult = await this.usersService.findUserByIdenfitier({ - username, - email, - }); - - if (userResult.isErr()) { - return Err(new InvalidAuthenticationCredentials()); - } - const user = userResult.unwrap(); - const passwordIsValid = await this.passwordService.validatePassword( - password, - user.password - ); - - if (!passwordIsValid) { - return Err(new InvalidAuthenticationCredentials()); - } - - const tokens = this.tokenFactory.generateTokens({ - userId: user.id, - }); - - this.logger.verbose(`User with ${user.email} email is authenticated`); - - return Ok(new AuthenticationCredentialDto({ userId: user.id, tokens })); - } -} diff --git a/libs/core/security/src/lib/authentication/application/commands/signin/signin.command.ts b/libs/core/security/src/lib/authentication/application/commands/signin/signin.command.ts deleted file mode 100644 index 59018b6..0000000 --- a/libs/core/security/src/lib/authentication/application/commands/signin/signin.command.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Command, CommandProps } from '@goran/common'; - -/** - * Signup - User Registration - * - * @param fullname - * @param username - * @param password - * @param email - */ -export class SigninCommand extends Command { - readonly email?: string; - readonly username?: string; - readonly password: string; - - constructor(props: CommandProps) { - super(props); - this.username = props.username; - this.email = props.email; - this.password = props.password; - } -} diff --git a/libs/core/security/src/lib/authentication/application/commands/signup/signup.command-handler.ts b/libs/core/security/src/lib/authentication/application/commands/signup/signup.command-handler.ts deleted file mode 100644 index 7fc1421..0000000 --- a/libs/core/security/src/lib/authentication/application/commands/signup/signup.command-handler.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { SignupCommand } from './signup.command'; -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { PasswordService } from '../../services'; -import { UsersService } from '@goran/users'; -import { Ok, Result } from 'oxide.ts'; -import { ExceptionBase } from '@goran/common'; -import { AuthenticationCredentialDto } from '../../dtos'; -import { AuthenticationTokenFactory } from '../../factories'; - -@CommandHandler(SignupCommand) -export class SignupCommandHandler implements ICommandHandler { - constructor( - private tokenFactory: AuthenticationTokenFactory, - private passwordService: PasswordService, - private usersService: UsersService - ) { } - private readonly logger = new Logger(SignupCommand.name); - - async execute( - command: SignupCommand - ): Promise> { - const hashedPassword = this.passwordService.hashPassword( - command.password - ); - const userResult = await this.usersService.create({ - ...command, - password: hashedPassword, - }); - - if (userResult.isErr()) { - return userResult; - } - - const user = userResult.unwrap(); - const tokens = this.tokenFactory.generateTokens({ - userId: user.getProps().id, - }); - - this.logger.verbose(`User with ${user.email} email signed up`); - - return Ok(new AuthenticationCredentialDto({ userId: user.id, tokens })); - } -} diff --git a/libs/core/security/src/lib/authentication/application/commands/signup/signup.command.ts b/libs/core/security/src/lib/authentication/application/commands/signup/signup.command.ts deleted file mode 100644 index 008b43c..0000000 --- a/libs/core/security/src/lib/authentication/application/commands/signup/signup.command.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Command, CommandProps } from '@goran/common'; - -/** - * Signup - User Registration - * - * @param fullname - * @param username - * @param password - * @param email - */ -export class SignupCommand extends Command { - readonly email: string; - readonly fullname?: string | null; - readonly username: string; - readonly password: string; - - constructor(props: CommandProps) { - super(props); - this.fullname = props.fullname; - this.username = props.username; - this.email = props.email; - this.password = props.password; - } -} diff --git a/libs/core/security/src/lib/authentication/application/dtos/authentication-credential.dto.ts b/libs/core/security/src/lib/authentication/application/dtos/authentication-credential.dto.ts index 8d6f947..6e05234 100644 --- a/libs/core/security/src/lib/authentication/application/dtos/authentication-credential.dto.ts +++ b/libs/core/security/src/lib/authentication/application/dtos/authentication-credential.dto.ts @@ -1,4 +1,4 @@ -import { TokenProps, TokenValueObject } from '../../domain'; +import { TokenProps, TokenValueObject } from '../../../tokens'; export class AuthenticationCredentialDto { userId: string; diff --git a/libs/core/security/src/lib/authentication/application/dtos/client-info.dto.ts b/libs/core/security/src/lib/authentication/application/dtos/client-info.dto.ts new file mode 100644 index 0000000..133ad3e --- /dev/null +++ b/libs/core/security/src/lib/authentication/application/dtos/client-info.dto.ts @@ -0,0 +1,6 @@ +import { Optional } from '@goran/common'; + +export interface ClientInfoDto { + userAgent: Optional; + ip: string; +} diff --git a/libs/core/security/src/lib/authentication/application/dtos/index.ts b/libs/core/security/src/lib/authentication/application/dtos/index.ts index 01f0ab6..cad9754 100644 --- a/libs/core/security/src/lib/authentication/application/dtos/index.ts +++ b/libs/core/security/src/lib/authentication/application/dtos/index.ts @@ -1 +1,2 @@ -export * from "./authentication-credential.dto"; +export * from './authentication-credential.dto'; +export * from './client-info.dto'; diff --git a/libs/core/security/src/lib/authentication/application/guards/index.ts b/libs/core/security/src/lib/authentication/application/guards/index.ts index 6c9091b..3525e2d 100644 --- a/libs/core/security/src/lib/authentication/application/guards/index.ts +++ b/libs/core/security/src/lib/authentication/application/guards/index.ts @@ -1,2 +1 @@ export * from './jwt-auth.guard'; -export * from './jwt-password-reset-session.guard'; diff --git a/libs/core/security/src/lib/authentication/application/guards/jwt-auth.guard.ts b/libs/core/security/src/lib/authentication/application/guards/jwt-auth.guard.ts index 105fd76..a247ca3 100644 --- a/libs/core/security/src/lib/authentication/application/guards/jwt-auth.guard.ts +++ b/libs/core/security/src/lib/authentication/application/guards/jwt-auth.guard.ts @@ -5,15 +5,21 @@ import { Logger, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { AuthenticationTokenService } from '../services'; import { Request } from 'express'; import { UnauthorizedException } from '@nestjs/common'; -import { InvalidTokenError } from '../../domain/errors'; +import { + InvalidTokenError, + TokensService, +} from '../../../tokens'; +import { SessionsService } from '../../../sessions'; @Injectable() -export class JwtAuthGuard extends AuthGuard('local') implements CanActivate { +export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate { logger = new Logger(JwtAuthGuard.name); - constructor(private readonly tokenService: AuthenticationTokenService) { + constructor( + private readonly tokenService: TokensService, + private readonly sessionsService: SessionsService + ) { super(); } @@ -24,19 +30,19 @@ export class JwtAuthGuard extends AuthGuard('local') implements CanActivate { try { const user = await this.tokenService.getUserFromToken(token); if (user.isNone()) { - throw new InvalidTokenError('Invalid token provided'); + throw new InvalidTokenError(); } - const isTokenRevoked = await this.tokenService.isTokenRevoked( - token - ); - if (isTokenRevoked === true) { - throw new UnauthorizedException('Token has been revoked'); + const isTokenRevoked = + await this.sessionsService.isRefreshTokenRevoked(token); + + if (isTokenRevoked.isErr() || isTokenRevoked.unwrap()) { + throw new UnauthorizedException(); } + req.user = user; return true; } catch (error) { - this.logger.error(error); - throw new UnauthorizedException('Invalid user'); + throw new InvalidTokenError(); } } } diff --git a/libs/core/security/src/lib/authentication/application/index.ts b/libs/core/security/src/lib/authentication/application/index.ts index 20e3519..813d01a 100644 --- a/libs/core/security/src/lib/authentication/application/index.ts +++ b/libs/core/security/src/lib/authentication/application/index.ts @@ -1,8 +1,5 @@ export * from './guards'; -export * from './services'; export * from './commands'; export * from './guards'; -export * from './validations'; export * from './strategies'; export * from './dtos'; -export * from './mappers'; diff --git a/libs/core/security/src/lib/authentication/application/jwt-payloads/index.ts b/libs/core/security/src/lib/authentication/application/jwt-payloads/index.ts index b32e5b3..7f18b91 100644 --- a/libs/core/security/src/lib/authentication/application/jwt-payloads/index.ts +++ b/libs/core/security/src/lib/authentication/application/jwt-payloads/index.ts @@ -1,2 +1 @@ export * from "./jwt-auth.payload" -export * from "./jwt-password-reset.payload" diff --git a/libs/core/security/src/lib/authentication/application/services/authentication-token.service.ts b/libs/core/security/src/lib/authentication/application/services/authentication-token.service.ts deleted file mode 100644 index 104038a..0000000 --- a/libs/core/security/src/lib/authentication/application/services/authentication-token.service.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; -import { InvalidTokenError, TokenValueObject } from '../../domain'; -import { UsersService } from '@goran/users'; -import { CONFIG_APP } from '@goran/config'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; -import { AggregateID } from '@goran/common'; -import { AuthenticationTokenFactory } from '../factories'; -import { Request } from 'express'; - -enum TokenState { - REVOKED = 'REVOKED', -} - -@Injectable() -export class AuthenticationTokenService { - constructor( - private readonly jwtService: JwtService, - private readonly configService: ConfigService, - private readonly usersService: UsersService, - private readonly authTokenFactory: AuthenticationTokenFactory, - @Inject(CACHE_MANAGER) private readonly cacheManager: Cache - ) { } - - private readonly refreshIn = this.configService.get( - CONFIG_APP.SECURITY_REFRESH_IN - ); - - async getUserFromToken(token: string) { - const id = this.jwtService.decode(token)['userId']; - const user = await this.usersService.findOneById(id); - return user; - } - - private async verifyRefreshToken(refreshToken: string): Promise { - try { - const decodedRefreshToken = this.jwtService.decode(refreshToken); - return !!decodedRefreshToken; - } catch (error) { - return false; - } - } - - async isTokenRevoked(token: string): Promise { - const revokedToken = await this.cacheManager.get(token); - const isRevoken = revokedToken - ? revokedToken === TokenState.REVOKED - : false; - return isRevoken; - } - - async refreshAccessToken( - userId: AggregateID, - refreshToken: string - ): Promise { - const isRefreshTokenValid = await this.verifyRefreshToken(refreshToken); - if (!isRefreshTokenValid) { - throw new InvalidTokenError('Invalid refresh token'); - } - - const isRefreshTokenRevoked = await this.isTokenRevoked(refreshToken); - if (isRefreshTokenRevoked) { - throw new InvalidTokenError('Invalid refresh provided'); - } - - return this.authTokenFactory.generateTokens({ userId }); - } - - async revokeToken(token: string) { - await this.cacheManager.set(token, TokenState.REVOKED, this.refreshIn); - } - - extractTokenFromRequest(req: Request) { - const token = req.headers?.authorization?.split(' ')[1]; - if (!token) { - throw new UnauthorizedException('Token must be provided'); - } - return token; - } -} diff --git a/libs/core/security/src/lib/authentication/application/services/index.ts b/libs/core/security/src/lib/authentication/application/services/index.ts deleted file mode 100644 index 1000c8d..0000000 --- a/libs/core/security/src/lib/authentication/application/services/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './authentication-token.service'; -export * from './password-reset-session.service'; -export * from './password'; diff --git a/libs/core/security/src/lib/authentication/application/services/password/index.ts b/libs/core/security/src/lib/authentication/application/services/password/index.ts deleted file mode 100644 index 4b7af89..0000000 --- a/libs/core/security/src/lib/authentication/application/services/password/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './password.service'; diff --git a/libs/core/security/src/lib/authentication/application/services/password/password.service.ts b/libs/core/security/src/lib/authentication/application/services/password/password.service.ts deleted file mode 100644 index 88fe4da..0000000 --- a/libs/core/security/src/lib/authentication/application/services/password/password.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { compare, genSaltSync, hashSync } from 'bcrypt'; - -/** - * @class PasswordService - * - */ -@Injectable() -export class PasswordService { - private readonly saltOrRounds = 10; - - get bcryptSaltRounds() { - return this.saltOrRounds; - } - - /** - * Validating correctness of the password with another password - * @param password - Unhashed raw password - * @param hashedPassword - Existing hashed password - * @returns boolean - True if password is the same as hashed password - */ - async validatePassword( - password: string, - hashedPassword: string - ): Promise { - return await compare(password, hashedPassword); - } - - /** - * @param password - Unhashed raw password - * @returns hashed version of the password - */ - hashPassword(password: string): string { - const salt = genSaltSync(this.saltOrRounds); - return hashSync(password, salt); - } -} diff --git a/libs/core/security/src/lib/authentication/application/strategies/index.ts b/libs/core/security/src/lib/authentication/application/strategies/index.ts index 4fe208c..e106f0d 100644 --- a/libs/core/security/src/lib/authentication/application/strategies/index.ts +++ b/libs/core/security/src/lib/authentication/application/strategies/index.ts @@ -1,2 +1 @@ export * from './jwt-auth.strategy'; -export * from './password-reset.strategy'; diff --git a/libs/core/security/src/lib/authentication/application/strategies/jwt-auth.strategy.ts b/libs/core/security/src/lib/authentication/application/strategies/jwt-auth.strategy.ts index 4b14814..658dce3 100644 --- a/libs/core/security/src/lib/authentication/application/strategies/jwt-auth.strategy.ts +++ b/libs/core/security/src/lib/authentication/application/strategies/jwt-auth.strategy.ts @@ -16,9 +16,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) { ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: `${configService.get( + secretOrKey: configService.get( CONFIG_APP.JWT_ACCESS_SECRET - )}`, + ), + ignoreExpiration: true, }); } diff --git a/libs/core/security/src/lib/authentication/application/validations/index.ts b/libs/core/security/src/lib/authentication/application/validations/index.ts deleted file mode 100644 index 59ca87b..0000000 --- a/libs/core/security/src/lib/authentication/application/validations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './is-password.decorator'; -export * from './is-username.decorator'; diff --git a/libs/core/security/src/lib/authentication/application/validations/is-username.decorator.ts b/libs/core/security/src/lib/authentication/application/validations/is-username.decorator.ts deleted file mode 100644 index fede893..0000000 --- a/libs/core/security/src/lib/authentication/application/validations/is-username.decorator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { registerDecorator, ValidationOptions } from 'class-validator'; - -/** - * Validates that a username correctness. - */ -export function IsUsername(validationOptions?: ValidationOptions) { - return function (object: object, propertyName: string) { - registerDecorator({ - name: 'IsUsername', - target: object.constructor, - propertyName: propertyName, - options: validationOptions, - validator: { - validate(value: string) { - return /^[a-z0-9_.-]{3,17}$/.test(value); - }, - defaultMessage() { - return "Only username that contain lowercase letters, numbers, '_', '-' and '.' with min 3 max 17 length"; - }, - }, - }); - }; -} diff --git a/libs/core/security/src/lib/authentication/authentication.module.ts b/libs/core/security/src/lib/authentication/authentication.module.ts index 404b165..12963d7 100644 --- a/libs/core/security/src/lib/authentication/authentication.module.ts +++ b/libs/core/security/src/lib/authentication/authentication.module.ts @@ -1,78 +1,22 @@ -import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { UsersModule } from '@goran/users'; +import { Global, Module } from '@nestjs/common'; +import { AuthenticationController } from './presenters/http'; import { - AuthenticationController, - AuthenticationTokenController, - PasswordResetController, -} from './presenters/http'; -import { - AuthenticationTokenService, - PasswordService, - PasswordResetSessionService, -} from './application/services'; -import { - RequestPasswordResetCommandHandler, - VerifyPasswordResetAttemptCommandHandler, - ResetPasswordCommandHandler, - SignupCommandHandler, - SigninCommandHandler, -} from './application/commands'; -import { CacheModule } from '@nestjs/cache-manager'; -import { MailModule } from '@goran/mail'; -import { CqrsModule } from '@nestjs/cqrs'; -import { - AuthenticationTokenFactory, - PasswordResetTokenFactory, -} from './application/factories'; -import { PasswordResetRequestMapper } from './application/mappers'; -import { PasswordResetRequestRepository } from './application/ports'; -import { PostgreSqlDrizzlePasswordResetRequestRepository } from './infrastructure/persistence/postgres-password-reset-request.repository'; - -const factories = [PasswordResetTokenFactory, AuthenticationTokenFactory]; -const services = [ - AuthenticationTokenService, - PasswordService, - PasswordResetSessionService, -]; -const mappers = [PasswordResetRequestMapper]; - -const repo = { - provide: PasswordResetRequestRepository, - useClass: PostgreSqlDrizzlePasswordResetRequestRepository, -}; - -const commandHanlders = [ - SignupCommandHandler, - SigninCommandHandler, - RequestPasswordResetCommandHandler, - ResetPasswordCommandHandler, - VerifyPasswordResetAttemptCommandHandler, -]; -const controllers = [ - AuthenticationController, - PasswordResetController, - AuthenticationTokenController, -]; - -const providers = [ - ...mappers, - ...commandHanlders, - ...services, - ...factories, - repo, -]; + SignUpCommandHandler, + SignInCommandHandler, + SignOutCommandHandler, + JwtStrategy, +} from './application'; +import { SessionsController } from '../sessions/presenters/http/sessions'; +@Global() @Module({ - imports: [ - UsersModule, - CqrsModule, - JwtModule.register({ global: true }), - CacheModule.register({ isGlobal: true }), - MailModule, + imports: [], + providers: [ + SignOutCommandHandler, + SignUpCommandHandler, + SignInCommandHandler, + JwtStrategy, ], - providers, - exports: [...services], - controllers, + controllers: [SessionsController, AuthenticationController], }) -export class AuthenticationModule { } +export class AuthenticationModule {} diff --git a/libs/core/security/src/lib/authentication/domain/errors/index.ts b/libs/core/security/src/lib/authentication/domain/errors/index.ts index f4d7e25..aeba2f9 100644 --- a/libs/core/security/src/lib/authentication/domain/errors/index.ts +++ b/libs/core/security/src/lib/authentication/domain/errors/index.ts @@ -1,7 +1 @@ -export * from './invalid-token.error'; -export * from './invalid-password-reset-request.error'; export * from './invalid-authentication-crednetials.error'; -export * from './invalid-otpcode.error'; -export * from './request-must-be-verified.error'; -export * from './request-is-already-verified.error'; -export * from './password-reset-request-attempt-invalidated.error'; diff --git a/libs/core/security/src/lib/authentication/domain/errors/invalid-token.error.ts b/libs/core/security/src/lib/authentication/domain/errors/invalid-token.error.ts deleted file mode 100644 index 1accd5e..0000000 --- a/libs/core/security/src/lib/authentication/domain/errors/invalid-token.error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class InvalidTokenError extends Error { - constructor(message: string) { - super(message); - this.name = 'InvalidTokenError'; - } -} diff --git a/libs/core/security/src/lib/authentication/domain/index.ts b/libs/core/security/src/lib/authentication/domain/index.ts index ce8e3cb..f72bc43 100644 --- a/libs/core/security/src/lib/authentication/domain/index.ts +++ b/libs/core/security/src/lib/authentication/domain/index.ts @@ -1,3 +1 @@ export * from './errors'; -export * from './value-objects'; -export * from './aggregates'; diff --git a/libs/core/security/src/lib/authentication/domain/value-objects/index.ts b/libs/core/security/src/lib/authentication/domain/value-objects/index.ts deleted file mode 100644 index abeaa6b..0000000 --- a/libs/core/security/src/lib/authentication/domain/value-objects/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './token.value-object'; -export * from './otpcode.value-object'; -export * from './password-reset-token.value-object'; diff --git a/libs/core/security/src/lib/authentication/domain/value-objects/token.value-object.ts b/libs/core/security/src/lib/authentication/domain/value-objects/token.value-object.ts deleted file mode 100644 index 26cafb1..0000000 --- a/libs/core/security/src/lib/authentication/domain/value-objects/token.value-object.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ArgumentInvalidException, Guard, ValueObject } from '@goran/common'; - -export interface TokenProps { - accessToken: string; - refreshToken: string; -} - -export class TokenValueObject extends ValueObject { - get accessToken() { - return this.props.accessToken; - } - - get refreshToken() { - return this.props.accessToken; - } - - protected override validate(props: TokenProps): void { - if (Guard.isEmpty(props.accessToken) || Guard.isEmpty(props.refreshToken)) - throw new ArgumentInvalidException( - 'Neither access token and refresh token can be empty' - ); - } -} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication-token/authentication-token.controller.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication-token/authentication-token.controller.ts deleted file mode 100644 index 8272ac8..0000000 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication-token/authentication-token.controller.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Body, Controller, Post, Req } from '@nestjs/common'; -import { RefreshTokenInput } from './refresh-token.dto'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiDocsAuthentication } from '../swagger'; -import { CurrentUser } from '@goran/users'; -import { Request } from 'express'; -import { InvalidTokenError } from '../../../domain'; -import { AuthenticationTokenService } from '../../../application'; - -@ApiTags('auth', 'token') -@Controller('auth/token') -export class AuthenticationTokenController { - constructor(private readonly tokenService: AuthenticationTokenService) {} - - @ApiOperation({ - summary: ApiDocsAuthentication.operationsSummary.refreshToken, - }) - @ApiOkResponse() - @Post('refresh') - async refreshToken( - @Req() req: Request, - @Body() refershTokenInput: RefreshTokenInput, - @CurrentUser() userId: string - ) { - const refreshToken = - refershTokenInput.token || req.cookies.refreshToken; - if (!refreshToken) { - return new InvalidTokenError('Invalid token provided'); - } - const newToken = await this.tokenService.refreshAccessToken( - userId, - refreshToken - ); - return { accessToken: newToken }; - } -} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication-token/index.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication-token/index.ts deleted file mode 100644 index c6e9add..0000000 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication-token/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './authentication-token.controller'; diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/authentication.controller.spec.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication.controller.spec.ts similarity index 95% rename from libs/core/security/src/lib/authentication/presenters/http/authentication/authentication.controller.spec.ts rename to libs/core/security/src/lib/authentication/presenters/http/authentication.controller.spec.ts index 3fb79a3..b2adc48 100644 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/authentication.controller.spec.ts +++ b/libs/core/security/src/lib/authentication/presenters/http/authentication.controller.spec.ts @@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthenticationController } from './authentication.controller'; import { AuthenticationPasswordService, - AuthenticationService, AuthenticationTokenService, } from '../services'; import { UsersModule } from '@goran/users'; @@ -22,7 +21,6 @@ describe('AuthenticationController', () => { CacheModule.register({ isGlobal: true }), ], providers: [ - AuthenticationService, AuthenticationTokenService, AuthenticationPasswordService, ], diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication.controller.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication.controller.ts new file mode 100644 index 0000000..d9b0624 --- /dev/null +++ b/libs/core/security/src/lib/authentication/presenters/http/authentication.controller.ts @@ -0,0 +1,131 @@ +import { + Body, + ClassSerializerInterceptor, + ConflictException as ConflictHttpException, + Controller, + Post, + Req, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { SignInDto } from './signin.dto'; +import { SignUpDto } from './signup.dto'; +import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiDocsAuthentication } from './swagger'; +import { CommandBus } from '@nestjs/cqrs'; +import { + AuthenticationCredentialDto, + JwtAuthGuard, + SignInCommand, + SignOutCommand, + SignUpCommand, +} from '../../application'; +import { CurrentUser, UserAlreadyExistsError, UserEntity } from '@goran/users'; +import { Result, match } from 'oxide.ts'; +import { ExceptionBase } from '@goran/common'; +import { SignInSuccessResponse } from './signin.response'; +import { SignUpSuccessResponse } from './signup.resonse'; +import {SignOutSuccessResponse} from './sign-out.resonse'; +import { Request } from 'express'; +import { UserAgent } from '@goran/common'; +import { TokensService } from '../../../tokens'; + +@ApiTags('auth') +@UseInterceptors(ClassSerializerInterceptor) +@Controller('auth') +export class AuthenticationController { + constructor( + private readonly commandBus: CommandBus, + private readonly tokensService: TokensService, + ) {} + + @ApiOkResponse({ type: SignUpSuccessResponse }) + @ApiOperation({ summary: ApiDocsAuthentication.operationsSummary.signup }) + @Post('sign-up') + async signUp( + @Body() body: SignUpDto, + @Req() req: Request, + @UserAgent() userAgent: string + ) { + const clientInfo = { + ip: req.ip!, + userAgent, + }; + const result: Result = + await this.commandBus.execute( + new SignUpCommand({ ...body, clientInfo }) + ); + + match(result, { + Ok: (credential: AuthenticationCredentialDto) => { + return new SignUpSuccessResponse({ + accessToken: credential.tokens.accessToken, + refreshToken: credential.tokens.refreshToken, + userId: credential.userId, + }); + }, + Err: (error: Error) => { + if (error instanceof UserAlreadyExistsError) + throw new ConflictHttpException(error); + throw error; + }, + }); + } + + @ApiOkResponse({ type: SignInSuccessResponse }) + @ApiOperation({ summary: ApiDocsAuthentication.operationsSummary.signin }) + @Post('sign-in') + async signIn( + @Body() body: SignInDto, + @Req() req: Request, + @UserAgent() userAgent: string + ) { + const clientInfo = { + ip: req.ip!, + userAgent, + }; + const result: Result = + await this.commandBus.execute( + new SignInCommand({ ...body, clientInfo }) + ); + + match(result, { + Ok: (credential: AuthenticationCredentialDto) => { + return new SignInSuccessResponse({ + accessToken: credential.tokens.accessToken, + refreshToken: credential.tokens.refreshToken, + userId: credential.userId, + }); + }, + Err: (error: Error) => { + if (error instanceof ExceptionBase) + throw new ConflictHttpException(error); + throw error; + }, + }); + } + + @ApiOkResponse({ type: SignOutSuccessResponse }) + @ApiOperation({ summary: ApiDocsAuthentication.operationsSummary.signout }) + @UseGuards(JwtAuthGuard) + @Post('sign-out') + async signOut(@Req() req: Request, @CurrentUser() user: UserEntity) { + const signOutResult: Result = await this.commandBus.execute( + new SignOutCommand({ + userId: user.id, + refreshToken: this.tokensService.extractTokenFromRequest(req) + }) + ); + + match(signOutResult, { + Ok: () => { + return new SignOutSuccessResponse(); + }, + Err: (error: Error) => { + if (error instanceof ExceptionBase) + throw new ConflictHttpException(error); + throw error; + }, + }); + } +} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/authentication.controller.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication/authentication.controller.ts deleted file mode 100644 index fc8d170..0000000 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/authentication.controller.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - Body, - Res, - ClassSerializerInterceptor, - ConflictException as ConflictHttpException, - Controller, - Post, - UseInterceptors, -} from '@nestjs/common'; -import { SignInDto } from './signin.dto'; -import { SignUpDto } from './signup.dto'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiDocsAuthentication } from '../swagger'; -import { CommandBus } from '@nestjs/cqrs'; -import { - AuthenticationCredentialDto, - SigninCommand, - SignupCommand, -} from '../../../application'; -import { UserAlreadyExistsError } from '@goran/users'; -import { Result, match } from 'oxide.ts'; -import { ExceptionBase } from '@goran/common'; -import { Response } from 'express'; -import { SignInSuccessResponse } from './signin.response'; -import { SignUpSuccessResponse } from './signup.resonse'; - -@ApiTags('auth') -@UseInterceptors(ClassSerializerInterceptor) -@Controller('auth') -export class AuthenticationController { - constructor(private readonly commandBus: CommandBus) { } - - @ApiOkResponse({ type: SignUpSuccessResponse }) - @ApiOperation({ summary: ApiDocsAuthentication.operationsSummary.signup }) - @Post('signup') - async signUp(@Body() body: SignUpDto, @Res() res: Response) { - const result: Result = - await this.commandBus.execute(new SignupCommand(body)); - - match(result, { - Ok: (credential: AuthenticationCredentialDto) => { - res.cookie('refreshToken', credential.tokens.refreshToken, { - httpOnly: true, - secure: true, - sameSite: 'none', - }); - - res.send( - new SignUpSuccessResponse({ - accessToken: credential.tokens.accessToken, - userId: credential.userId, - }) - ); - }, - Err: (error: Error) => { - if (error instanceof UserAlreadyExistsError) - throw new ConflictHttpException(error); - throw error; - }, - }); - } - - @ApiOkResponse({ type: SignInSuccessResponse }) - @ApiOperation({ summary: ApiDocsAuthentication.operationsSummary.signin }) - @Post('signin') - async signIn(@Body() body: SignInDto, @Res() res: Response) { - const result: Result = - await this.commandBus.execute(new SigninCommand(body)); - - match(result, { - Ok: (credential: AuthenticationCredentialDto) => { - res.cookie('refresh_token', credential.tokens.refreshToken, { - httpOnly: true, - secure: true, - sameSite: 'none', - }); - - res.send( - new SignInSuccessResponse({ - accessToken: credential.tokens.accessToken, - userId: credential.userId, - }) - ); - }, - Err: (error: Error) => { - if (error instanceof ExceptionBase) - throw new ConflictHttpException(error); - throw error; - }, - }); - } -} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/index.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication/index.ts deleted file mode 100644 index 2b73523..0000000 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './authentication.controller'; diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/signin.dto.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication/signin.dto.ts deleted file mode 100644 index 25ff685..0000000 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/signin.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IsEmail, IsOptional, IsDefined } from 'class-validator'; -import { IsPassword, IsUsername } from '../../../application'; - -export class SignInDto { - @IsPassword() - @IsDefined() - password: string; - - @IsOptional() - @IsEmail() - email?: string; - - @IsOptional() - @IsUsername() - username?: string; -} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/signup.dto.ts b/libs/core/security/src/lib/authentication/presenters/http/authentication/signup.dto.ts deleted file mode 100644 index f36a93b..0000000 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/signup.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IsEmail, IsDefined, IsOptional } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsPassword, IsUsername } from '../../../application'; - -export class SignUpDto { - @IsEmail() - @ApiProperty() - email: string; - - @IsPassword() - @IsDefined() - @ApiProperty() - password: string; - - @ApiProperty() - @IsDefined() - @IsUsername() - username: string; - - @IsOptional() - @ApiPropertyOptional() - fullname?: string | null; -} diff --git a/libs/core/security/src/lib/authentication/presenters/http/index.ts b/libs/core/security/src/lib/authentication/presenters/http/index.ts index 55e9285..a9002e6 100644 --- a/libs/core/security/src/lib/authentication/presenters/http/index.ts +++ b/libs/core/security/src/lib/authentication/presenters/http/index.ts @@ -1,3 +1,2 @@ -export * from './authentication'; -export * from './authentication-token'; -export * from './password-reset'; +export * from './authentication.controller'; + diff --git a/libs/core/security/src/lib/authentication/presenters/http/sign-out.resonse.ts b/libs/core/security/src/lib/authentication/presenters/http/sign-out.resonse.ts new file mode 100644 index 0000000..5cea537 --- /dev/null +++ b/libs/core/security/src/lib/authentication/presenters/http/sign-out.resonse.ts @@ -0,0 +1,15 @@ +import { ResponseBase } from '@goran/common'; + + +export class SignOutSuccessResponse< + SignOutResponseProps +> extends ResponseBase { + constructor() { + super({ + message: 'Signed out successfully', + desc: 'Your session is obsolete and token is expired.', + success: true, + code: 'SIGNOUT_SUCCESS', + }); + } +} diff --git a/libs/core/security/src/lib/authentication/presenters/http/signin.dto.ts b/libs/core/security/src/lib/authentication/presenters/http/signin.dto.ts new file mode 100644 index 0000000..c463670 --- /dev/null +++ b/libs/core/security/src/lib/authentication/presenters/http/signin.dto.ts @@ -0,0 +1,18 @@ +import { IsEmail, IsOptional, IsDefined } from 'class-validator'; +import { IsPassword } from '../../../password'; +import { IsUsername } from '@goran/users'; +import { Optional } from '@goran/common'; + +export class SignInDto { + @IsPassword() + @IsDefined() + password: string; + + @IsOptional() + @IsEmail() + email: Optional; + + @IsOptional() + @IsUsername() + username: Optional; +} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/signin.response.ts b/libs/core/security/src/lib/authentication/presenters/http/signin.response.ts similarity index 94% rename from libs/core/security/src/lib/authentication/presenters/http/authentication/signin.response.ts rename to libs/core/security/src/lib/authentication/presenters/http/signin.response.ts index 445cce3..240df67 100644 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/signin.response.ts +++ b/libs/core/security/src/lib/authentication/presenters/http/signin.response.ts @@ -2,6 +2,7 @@ import { ResponseBase } from '@goran/common'; interface SignInResponseProps { accessToken: string; + refreshToken: string; userId: string; } diff --git a/libs/core/security/src/lib/authentication/presenters/http/signup.dto.ts b/libs/core/security/src/lib/authentication/presenters/http/signup.dto.ts new file mode 100644 index 0000000..0ea877c --- /dev/null +++ b/libs/core/security/src/lib/authentication/presenters/http/signup.dto.ts @@ -0,0 +1,25 @@ +import { IsEmail, IsDefined, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsPassword } from '../../../password'; +import { IsUsername } from '@goran/users'; +import { Optional } from '@goran/common'; + +export class SignUpDto { + @IsEmail() + @ApiProperty() + email: string; + + @IsPassword() + @IsDefined() + @ApiProperty() + password: string; + + @ApiProperty() + @IsDefined() + @IsUsername() + username: string; + + @IsOptional() + @ApiPropertyOptional() + fullname: Optional; +} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication/signup.resonse.ts b/libs/core/security/src/lib/authentication/presenters/http/signup.resonse.ts similarity index 94% rename from libs/core/security/src/lib/authentication/presenters/http/authentication/signup.resonse.ts rename to libs/core/security/src/lib/authentication/presenters/http/signup.resonse.ts index 9b3195b..4e67078 100644 --- a/libs/core/security/src/lib/authentication/presenters/http/authentication/signup.resonse.ts +++ b/libs/core/security/src/lib/authentication/presenters/http/signup.resonse.ts @@ -2,6 +2,7 @@ import { ResponseBase } from '@goran/common'; interface SignUpResponseProps { accessToken: string; + refreshToken: string; userId: string; } diff --git a/libs/core/security/src/lib/index.ts b/libs/core/security/src/lib/index.ts index 86f36f3..7f29401 100644 --- a/libs/core/security/src/lib/index.ts +++ b/libs/core/security/src/lib/index.ts @@ -1,2 +1,6 @@ export * from './authentication'; -export * from './session-management'; +export * from './sessions'; +export * from './password-reset'; +export * from './tokens'; +export * from './password'; +export * from './security-options.interface'; diff --git a/libs/core/security/src/lib/password-reset/application/commands/index.ts b/libs/core/security/src/lib/password-reset/application/commands/index.ts new file mode 100644 index 0000000..e5bb961 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/application/commands/index.ts @@ -0,0 +1,3 @@ +export * from './request-password-reset'; +export * from './verify-password-reset-attempt'; +export * from './reset-password'; diff --git a/libs/core/security/src/lib/authentication/application/commands/request-password-reset/index.ts b/libs/core/security/src/lib/password-reset/application/commands/request-password-reset/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/request-password-reset/index.ts rename to libs/core/security/src/lib/password-reset/application/commands/request-password-reset/index.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/request-password-reset/request-user-password-reset.command-handler.ts b/libs/core/security/src/lib/password-reset/application/commands/request-password-reset/request-user-password-reset.command-handler.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/request-password-reset/request-user-password-reset.command-handler.ts rename to libs/core/security/src/lib/password-reset/application/commands/request-password-reset/request-user-password-reset.command-handler.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/request-password-reset/request-user-password-reset.command.ts b/libs/core/security/src/lib/password-reset/application/commands/request-password-reset/request-user-password-reset.command.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/request-password-reset/request-user-password-reset.command.ts rename to libs/core/security/src/lib/password-reset/application/commands/request-password-reset/request-user-password-reset.command.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/reset-password/index.ts b/libs/core/security/src/lib/password-reset/application/commands/reset-password/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/reset-password/index.ts rename to libs/core/security/src/lib/password-reset/application/commands/reset-password/index.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/reset-password/reset-password.command-handler.ts b/libs/core/security/src/lib/password-reset/application/commands/reset-password/reset-password.command-handler.ts similarity index 85% rename from libs/core/security/src/lib/authentication/application/commands/reset-password/reset-password.command-handler.ts rename to libs/core/security/src/lib/password-reset/application/commands/reset-password/reset-password.command-handler.ts index 0bc9b07..790a051 100644 --- a/libs/core/security/src/lib/authentication/application/commands/reset-password/reset-password.command-handler.ts +++ b/libs/core/security/src/lib/password-reset/application/commands/reset-password/reset-password.command-handler.ts @@ -1,15 +1,14 @@ import { Logger } from '@nestjs/common'; import { ResetPasswordCommand } from './reset-password.command'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { PasswordService } from '../../services'; +import { PasswordService } from '../../../../password/application'; import { UsersService } from '@goran/users'; import { PasswordResetSessionService } from '../../services/password-reset-session.service'; import { PasswordResetRequestRepository } from '../../ports'; @CommandHandler(ResetPasswordCommand) export class ResetPasswordCommandHandler - implements ICommandHandler -{ + implements ICommandHandler { private readonly logger = new Logger(ResetPasswordCommand.name); constructor( @@ -17,14 +16,14 @@ export class ResetPasswordCommandHandler private readonly repository: PasswordResetRequestRepository, private readonly passwordService: PasswordService, private readonly usersService: UsersService - ) {} + ) { } async execute(command: ResetPasswordCommand) { const requestAgg = await this.sessionService.getAggByToken( command.token ); - const hashedPassword = this.passwordService.hashPassword( + const hashedPassword = await this.passwordService.hashPassword( command.password ); diff --git a/libs/core/security/src/lib/authentication/application/commands/reset-password/reset-password.command.ts b/libs/core/security/src/lib/password-reset/application/commands/reset-password/reset-password.command.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/reset-password/reset-password.command.ts rename to libs/core/security/src/lib/password-reset/application/commands/reset-password/reset-password.command.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/index.ts b/libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/index.ts rename to libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/index.ts diff --git a/libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command-handler.ts b/libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command-handler.ts similarity index 96% rename from libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command-handler.ts rename to libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command-handler.ts index d596a1f..3c880e8 100644 --- a/libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command-handler.ts +++ b/libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command-handler.ts @@ -2,7 +2,7 @@ import { BadRequestException, Logger } from '@nestjs/common'; import { VerifyPasswordResetAttemptCommand } from './verify-password-reset-attempt.command'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { PasswordResetRequestRepository } from '../../ports'; -import { OtpCodeVO } from '../../../domain'; +import { OtpCodeVO } from '@goran/security'; import { PasswordResetSessionService } from '../../services/password-reset-session.service'; @CommandHandler(VerifyPasswordResetAttemptCommand) diff --git a/libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command.ts b/libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command.ts rename to libs/core/security/src/lib/password-reset/application/commands/verify-password-reset-attempt/verify-password-reset-attempt.command.ts diff --git a/libs/core/security/src/lib/authentication/application/factories/index.ts b/libs/core/security/src/lib/password-reset/application/factories/index.ts similarity index 50% rename from libs/core/security/src/lib/authentication/application/factories/index.ts rename to libs/core/security/src/lib/password-reset/application/factories/index.ts index 9a66fe7..ed36c45 100644 --- a/libs/core/security/src/lib/authentication/application/factories/index.ts +++ b/libs/core/security/src/lib/password-reset/application/factories/index.ts @@ -1,2 +1 @@ -export * from './authentication-token.factory'; export * from './password-reset-token.factory'; diff --git a/libs/core/security/src/lib/authentication/application/factories/password-reset-token.factory.ts b/libs/core/security/src/lib/password-reset/application/factories/password-reset-token.factory.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/factories/password-reset-token.factory.ts rename to libs/core/security/src/lib/password-reset/application/factories/password-reset-token.factory.ts diff --git a/libs/core/security/src/lib/password-reset/application/guards/index.ts b/libs/core/security/src/lib/password-reset/application/guards/index.ts new file mode 100644 index 0000000..1a41965 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/application/guards/index.ts @@ -0,0 +1 @@ +export * from './jwt-password-reset-session.guard'; diff --git a/libs/core/security/src/lib/authentication/application/guards/jwt-password-reset-session.guard.ts b/libs/core/security/src/lib/password-reset/application/guards/jwt-password-reset-session.guard.ts similarity index 80% rename from libs/core/security/src/lib/authentication/application/guards/jwt-password-reset-session.guard.ts rename to libs/core/security/src/lib/password-reset/application/guards/jwt-password-reset-session.guard.ts index 6e81e67..45a7114 100644 --- a/libs/core/security/src/lib/authentication/application/guards/jwt-password-reset-session.guard.ts +++ b/libs/core/security/src/lib/password-reset/application/guards/jwt-password-reset-session.guard.ts @@ -7,18 +7,17 @@ import { import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { UnauthorizedException } from '@nestjs/common'; -import { InvalidTokenError } from '../../domain/errors'; +import { InvalidTokenError, TokensService } from '@goran/security'; import { PasswordResetSessionService } from '../services/password-reset-session.service'; -import { AuthenticationTokenService } from '../services'; @Injectable() export class JwtPasswordResetSessionGuard - extends AuthGuard('password-reset-session') + extends AuthGuard('jwt-password-reset-session') implements CanActivate { logger = new Logger(JwtPasswordResetSessionGuard.name); constructor( - private readonly tokenService: AuthenticationTokenService, + private readonly tokenService: TokensService, private readonly sessionService: PasswordResetSessionService ) { super(); @@ -32,7 +31,7 @@ export class JwtPasswordResetSessionGuard const requestAgg = await this.sessionService.getAggByToken(token); const status = requestAgg.getProps().status; if (status === 'successful' || status === 'dismissed') { - throw new InvalidTokenError('Invalid token provided'); + throw new InvalidTokenError(); } return true; } catch (error) { diff --git a/libs/core/security/src/lib/password-reset/application/index.ts b/libs/core/security/src/lib/password-reset/application/index.ts new file mode 100644 index 0000000..f78a916 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/application/index.ts @@ -0,0 +1,8 @@ +export * from './commands'; +export * from './factories'; +export * from './guards'; +export * from './jwt-payloads'; +export * from './mappers'; +export * from './ports'; +export * from './services'; +export * from './strategies'; diff --git a/libs/core/security/src/lib/password-reset/application/jwt-payloads/index.ts b/libs/core/security/src/lib/password-reset/application/jwt-payloads/index.ts new file mode 100644 index 0000000..ee6dead --- /dev/null +++ b/libs/core/security/src/lib/password-reset/application/jwt-payloads/index.ts @@ -0,0 +1 @@ +export * from './jwt-password-reset.payload'; diff --git a/libs/core/security/src/lib/authentication/application/jwt-payloads/jwt-password-reset.payload.ts b/libs/core/security/src/lib/password-reset/application/jwt-payloads/jwt-password-reset.payload.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/jwt-payloads/jwt-password-reset.payload.ts rename to libs/core/security/src/lib/password-reset/application/jwt-payloads/jwt-password-reset.payload.ts diff --git a/libs/core/security/src/lib/authentication/application/mappers/index.ts b/libs/core/security/src/lib/password-reset/application/mappers/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/mappers/index.ts rename to libs/core/security/src/lib/password-reset/application/mappers/index.ts diff --git a/libs/core/security/src/lib/authentication/application/mappers/password-reset-request.mapper.ts b/libs/core/security/src/lib/password-reset/application/mappers/password-reset-request.mapper.ts similarity index 97% rename from libs/core/security/src/lib/authentication/application/mappers/password-reset-request.mapper.ts rename to libs/core/security/src/lib/password-reset/application/mappers/password-reset-request.mapper.ts index bb0602e..d17e13f 100644 --- a/libs/core/security/src/lib/authentication/application/mappers/password-reset-request.mapper.ts +++ b/libs/core/security/src/lib/password-reset/application/mappers/password-reset-request.mapper.ts @@ -1,10 +1,10 @@ import { Mapper } from '@goran/common'; import { Injectable } from '@nestjs/common'; import { - OtpCodeVO, PasswordResetRequestAggregate, PasswordResetTokenVO, } from '../../domain'; +import { OtpCodeVO } from '@goran/security'; import { PasswordResetRequestModel } from '../models/password-reset-request.model'; import { UserMapper, UsersService } from '@goran/users'; diff --git a/libs/core/security/src/lib/authentication/application/models/index.ts b/libs/core/security/src/lib/password-reset/application/models/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/models/index.ts rename to libs/core/security/src/lib/password-reset/application/models/index.ts diff --git a/libs/core/security/src/lib/authentication/application/models/password-reset-request.model.ts b/libs/core/security/src/lib/password-reset/application/models/password-reset-request.model.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/models/password-reset-request.model.ts rename to libs/core/security/src/lib/password-reset/application/models/password-reset-request.model.ts diff --git a/libs/core/security/src/lib/authentication/application/ports/index.ts b/libs/core/security/src/lib/password-reset/application/ports/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/ports/index.ts rename to libs/core/security/src/lib/password-reset/application/ports/index.ts diff --git a/libs/core/security/src/lib/authentication/application/ports/password-reset-request.repository.ts b/libs/core/security/src/lib/password-reset/application/ports/password-reset-request.repository.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/ports/password-reset-request.repository.ts rename to libs/core/security/src/lib/password-reset/application/ports/password-reset-request.repository.ts diff --git a/libs/core/security/src/lib/password-reset/application/services/index.ts b/libs/core/security/src/lib/password-reset/application/services/index.ts new file mode 100644 index 0000000..9224343 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/application/services/index.ts @@ -0,0 +1 @@ +export * from './password-reset-session.service'; diff --git a/libs/core/security/src/lib/authentication/application/services/password-reset-session.service.ts b/libs/core/security/src/lib/password-reset/application/services/password-reset-session.service.ts similarity index 88% rename from libs/core/security/src/lib/authentication/application/services/password-reset-session.service.ts rename to libs/core/security/src/lib/password-reset/application/services/password-reset-session.service.ts index 810a94b..be14d9d 100644 --- a/libs/core/security/src/lib/authentication/application/services/password-reset-session.service.ts +++ b/libs/core/security/src/lib/password-reset/application/services/password-reset-session.service.ts @@ -1,18 +1,17 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { PasswordResetRequestRepository } from '../ports'; -import { InvalidAuthenticationCredentials } from '../../domain'; +import { InvalidAuthenticationCredentials } from '../../../authentication/domain'; import { PasswordResetRequestMapper } from '../mappers'; @Injectable() export class PasswordResetSessionService { constructor( private readonly mapper: PasswordResetRequestMapper, - private readonly repository: PasswordResetRequestRepository) { } + private readonly repository: PasswordResetRequestRepository + ) {} async getAggByToken(token: string) { - const requestResult = await this.repository.findOneByToken( - token - ); + const requestResult = await this.repository.findOneByToken(token); if (requestResult.isNone()) throw new BadRequestException( diff --git a/libs/core/security/src/lib/password-reset/application/strategies/index.ts b/libs/core/security/src/lib/password-reset/application/strategies/index.ts new file mode 100644 index 0000000..1d10d7a --- /dev/null +++ b/libs/core/security/src/lib/password-reset/application/strategies/index.ts @@ -0,0 +1 @@ +export * from './password-reset.strategy'; diff --git a/libs/core/security/src/lib/authentication/application/strategies/password-reset.strategy.ts b/libs/core/security/src/lib/password-reset/application/strategies/password-reset.strategy.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/strategies/password-reset.strategy.ts rename to libs/core/security/src/lib/password-reset/application/strategies/password-reset.strategy.ts diff --git a/libs/core/security/src/lib/authentication/domain/aggregates/index.ts b/libs/core/security/src/lib/password-reset/domain/aggregates/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/aggregates/index.ts rename to libs/core/security/src/lib/password-reset/domain/aggregates/index.ts diff --git a/libs/core/security/src/lib/authentication/domain/aggregates/password-reset-request.aggregate.ts b/libs/core/security/src/lib/password-reset/domain/aggregates/password-reset-request.aggregate.ts similarity index 96% rename from libs/core/security/src/lib/authentication/domain/aggregates/password-reset-request.aggregate.ts rename to libs/core/security/src/lib/password-reset/domain/aggregates/password-reset-request.aggregate.ts index 3bc0598..47f6bea 100644 --- a/libs/core/security/src/lib/authentication/domain/aggregates/password-reset-request.aggregate.ts +++ b/libs/core/security/src/lib/password-reset/domain/aggregates/password-reset-request.aggregate.ts @@ -1,7 +1,8 @@ import { AggregateRoot, AggregateID, ExceptionBase } from '@goran/common'; import { ulid } from 'ulid'; import { UserEntity } from '@goran/users'; -import { OtpCodeVO, PasswordResetTokenVO } from '../value-objects'; +import { PasswordResetTokenVO } from '../value-objects'; +import { OtpCodeVO } from '@goran/security'; import { Err, Ok, Result } from 'oxide.ts'; import { InvalidOtpCodeError, diff --git a/libs/core/security/src/lib/password-reset/domain/errors/index.ts b/libs/core/security/src/lib/password-reset/domain/errors/index.ts new file mode 100644 index 0000000..4267fda --- /dev/null +++ b/libs/core/security/src/lib/password-reset/domain/errors/index.ts @@ -0,0 +1,5 @@ +export * from './invalid-password-reset-request.error'; +export * from './invalid-otpcode.error'; +export * from './request-must-be-verified.error'; +export * from './request-is-already-verified.error'; +export * from './password-reset-request-attempt-invalidated.error'; diff --git a/libs/core/security/src/lib/authentication/domain/errors/invalid-otpcode.error.ts b/libs/core/security/src/lib/password-reset/domain/errors/invalid-otpcode.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/errors/invalid-otpcode.error.ts rename to libs/core/security/src/lib/password-reset/domain/errors/invalid-otpcode.error.ts diff --git a/libs/core/security/src/lib/authentication/domain/errors/invalid-password-reset-request.error.ts b/libs/core/security/src/lib/password-reset/domain/errors/invalid-password-reset-request.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/errors/invalid-password-reset-request.error.ts rename to libs/core/security/src/lib/password-reset/domain/errors/invalid-password-reset-request.error.ts diff --git a/libs/core/security/src/lib/authentication/domain/errors/password-reset-request-attempt-invalidated.error.ts b/libs/core/security/src/lib/password-reset/domain/errors/password-reset-request-attempt-invalidated.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/errors/password-reset-request-attempt-invalidated.error.ts rename to libs/core/security/src/lib/password-reset/domain/errors/password-reset-request-attempt-invalidated.error.ts diff --git a/libs/core/security/src/lib/authentication/domain/errors/request-is-already-verified.error.ts b/libs/core/security/src/lib/password-reset/domain/errors/request-is-already-verified.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/errors/request-is-already-verified.error.ts rename to libs/core/security/src/lib/password-reset/domain/errors/request-is-already-verified.error.ts diff --git a/libs/core/security/src/lib/authentication/domain/errors/request-must-be-verified.error.ts b/libs/core/security/src/lib/password-reset/domain/errors/request-must-be-verified.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/errors/request-must-be-verified.error.ts rename to libs/core/security/src/lib/password-reset/domain/errors/request-must-be-verified.error.ts diff --git a/libs/core/security/src/lib/password-reset/domain/index.ts b/libs/core/security/src/lib/password-reset/domain/index.ts new file mode 100644 index 0000000..ce8e3cb --- /dev/null +++ b/libs/core/security/src/lib/password-reset/domain/index.ts @@ -0,0 +1,3 @@ +export * from './errors'; +export * from './value-objects'; +export * from './aggregates'; diff --git a/libs/core/security/src/lib/password-reset/domain/value-objects/index.ts b/libs/core/security/src/lib/password-reset/domain/value-objects/index.ts new file mode 100644 index 0000000..582f856 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/domain/value-objects/index.ts @@ -0,0 +1 @@ +export * from './password-reset-token.value-object'; \ No newline at end of file diff --git a/libs/core/security/src/lib/authentication/domain/value-objects/password-reset-token.value-object.ts b/libs/core/security/src/lib/password-reset/domain/value-objects/password-reset-token.value-object.ts similarity index 100% rename from libs/core/security/src/lib/authentication/domain/value-objects/password-reset-token.value-object.ts rename to libs/core/security/src/lib/password-reset/domain/value-objects/password-reset-token.value-object.ts diff --git a/libs/core/security/src/lib/password-reset/index.ts b/libs/core/security/src/lib/password-reset/index.ts new file mode 100644 index 0000000..5693382 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/index.ts @@ -0,0 +1,3 @@ +export * from './application'; +export * from './domain'; +export * from './password-reset.module'; diff --git a/libs/domain/users/src/infrastructure/index.ts b/libs/core/security/src/lib/password-reset/infrastructure/index.ts similarity index 100% rename from libs/domain/users/src/infrastructure/index.ts rename to libs/core/security/src/lib/password-reset/infrastructure/index.ts diff --git a/libs/core/security/src/lib/password-reset/infrastructure/persistence/index.ts b/libs/core/security/src/lib/password-reset/infrastructure/persistence/index.ts new file mode 100644 index 0000000..60ec224 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/infrastructure/persistence/index.ts @@ -0,0 +1 @@ +export * from './postgres-password-reset-request.repository'; diff --git a/libs/core/security/src/lib/authentication/infrastructure/persistence/postgres-password-reset-request.repository.ts b/libs/core/security/src/lib/password-reset/infrastructure/persistence/postgres-password-reset-request.repository.ts similarity index 100% rename from libs/core/security/src/lib/authentication/infrastructure/persistence/postgres-password-reset-request.repository.ts rename to libs/core/security/src/lib/password-reset/infrastructure/persistence/postgres-password-reset-request.repository.ts diff --git a/libs/core/security/src/lib/authentication/infrastructure/persistence/unable-delete-request.error.ts b/libs/core/security/src/lib/password-reset/infrastructure/persistence/unable-delete-request.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/infrastructure/persistence/unable-delete-request.error.ts rename to libs/core/security/src/lib/password-reset/infrastructure/persistence/unable-delete-request.error.ts diff --git a/libs/core/security/src/lib/authentication/infrastructure/persistence/unable-save-request.error.ts b/libs/core/security/src/lib/password-reset/infrastructure/persistence/unable-save-request.error.ts similarity index 100% rename from libs/core/security/src/lib/authentication/infrastructure/persistence/unable-save-request.error.ts rename to libs/core/security/src/lib/password-reset/infrastructure/persistence/unable-save-request.error.ts diff --git a/libs/core/security/src/lib/password-reset/password-reset.module.ts b/libs/core/security/src/lib/password-reset/password-reset.module.ts new file mode 100644 index 0000000..d2d3ef2 --- /dev/null +++ b/libs/core/security/src/lib/password-reset/password-reset.module.ts @@ -0,0 +1,37 @@ +import { Global, Module } from '@nestjs/common'; +import { + PasswordResetRequestMapper, + PasswordResetRequestRepository, + PasswordResetSessionService, + PasswordResetTokenFactory, + RequestPasswordResetCommandHandler, + ResetPasswordCommandHandler, + VerifyPasswordResetAttemptCommandHandler, +} from './application'; +import { PostgreSqlDrizzlePasswordResetRequestRepository } from './infrastructure'; +import { PasswordModule } from '../password'; + +const commandHandlers = [ + RequestPasswordResetCommandHandler, + VerifyPasswordResetAttemptCommandHandler, + ResetPasswordCommandHandler, +]; + +const providers = [ + ...commandHandlers, + { + provide: PasswordResetRequestRepository, + useClass: PostgreSqlDrizzlePasswordResetRequestRepository, + }, + PasswordResetSessionService, + PasswordResetRequestMapper, + PasswordResetTokenFactory, +]; + +@Global() +@Module({ + providers, + imports: [PasswordModule], + exports: [PasswordResetSessionService], +}) +export class PasswordResetModule { } diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/index.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/index.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/index.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/request-password-reset.dto.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/request-password-reset.dto.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/request-password-reset.dto.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/request-password-reset.dto.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/reset-password.dto.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/reset-password.dto.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/reset-password.dto.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/reset-password.dto.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/verify-password-reset-request.dto.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/verify-password-reset-request.dto.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/dtos/verify-password-reset-request.dto.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/dtos/verify-password-reset-request.dto.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/index.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/index.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/index.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/password-reset.controller.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/password-reset.controller.ts similarity index 82% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/password-reset.controller.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/password-reset.controller.ts index 9828833..32283ea 100644 --- a/libs/core/security/src/lib/authentication/presenters/http/password-reset/password-reset.controller.ts +++ b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/password-reset.controller.ts @@ -4,14 +4,11 @@ import { Post, UseGuards, Req, - Logger, BadRequestException, } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiDocsAuthentication } from '../swagger'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { CommandBus } from '@nestjs/cqrs'; import { - AuthenticationTokenService, JwtPasswordResetSessionGuard, RequestUserPassswordResetCommand, ResetPasswordCommand, @@ -32,22 +29,17 @@ import { Result, match } from 'oxide.ts'; import { ExceptionBase } from '@goran/common'; import { Request } from 'express'; import { PasswordResetRequestAggregate } from '../../../domain'; +import { TokensService } from '../../../../tokens'; @ApiTags('auth', 'password-reset') @Controller('auth/password-reset') export class PasswordResetController { constructor( private readonly commandBus: CommandBus, - private readonly tokenService: AuthenticationTokenService - ) {} - private readonly logger = new Logger( - VerifyPasswordResetAttemptCommand.name - ); + private readonly tokenService: TokensService + ) { } @ApiOkResponse() - @ApiOperation({ - summary: ApiDocsAuthentication.operationsSummary.passwordReset, - }) @Post('request') async reqResetPassword(@Body() credential: RequestPasswordResetDto) { const result: Result = @@ -65,9 +57,6 @@ export class PasswordResetController { } @ApiOkResponse() - @ApiOperation({ - summary: ApiDocsAuthentication.operationsSummary.passwordVerify, - }) @UseGuards(JwtPasswordResetSessionGuard) @Post('verify') async verifyPassword( @@ -95,9 +84,6 @@ export class PasswordResetController { } @ApiOkResponse() - @ApiOperation({ - summary: ApiDocsAuthentication.operationsSummary.passwordVerify, - }) @UseGuards(JwtPasswordResetSessionGuard) @Post('reset') async resetPassword( diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/index.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/index.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/index.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/request-password-reset.response.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/request-password-reset.response.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/request-password-reset.response.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/request-password-reset.response.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/reset-password.response.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/reset-password.response.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/reset-password.response.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/reset-password.response.ts diff --git a/libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/verify-password-reset-request.response.ts b/libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/verify-password-reset-request.response.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/password-reset/responses/verify-password-reset-request.response.ts rename to libs/core/security/src/lib/password-reset/presenters/http/password-reset/responses/verify-password-reset-request.response.ts diff --git a/libs/core/security/src/lib/password/application/decorators/index.ts b/libs/core/security/src/lib/password/application/decorators/index.ts new file mode 100644 index 0000000..177c8ea --- /dev/null +++ b/libs/core/security/src/lib/password/application/decorators/index.ts @@ -0,0 +1 @@ +export * from './is-password.decorator'; \ No newline at end of file diff --git a/libs/core/security/src/lib/authentication/application/validations/is-password.decorator.ts b/libs/core/security/src/lib/password/application/decorators/is-password.decorator.ts similarity index 97% rename from libs/core/security/src/lib/authentication/application/validations/is-password.decorator.ts rename to libs/core/security/src/lib/password/application/decorators/is-password.decorator.ts index 6713f64..531d42d 100644 --- a/libs/core/security/src/lib/authentication/application/validations/is-password.decorator.ts +++ b/libs/core/security/src/lib/password/application/decorators/is-password.decorator.ts @@ -31,7 +31,7 @@ export function IsPassword( ) { return function (object: object, propertyName: string) { registerDecorator({ - name: 'IsPasswordStrong', + name: 'IsPassword', target: object.constructor, propertyName: propertyName, options: validationOptions, diff --git a/libs/core/security/src/lib/password/application/index.ts b/libs/core/security/src/lib/password/application/index.ts new file mode 100644 index 0000000..7d9d4d6 --- /dev/null +++ b/libs/core/security/src/lib/password/application/index.ts @@ -0,0 +1,2 @@ +export * from './services'; +export * from './decorators'; diff --git a/libs/core/security/src/lib/password/application/services/index.ts b/libs/core/security/src/lib/password/application/services/index.ts new file mode 100644 index 0000000..bc21a38 --- /dev/null +++ b/libs/core/security/src/lib/password/application/services/index.ts @@ -0,0 +1 @@ +export * from './password.service'; \ No newline at end of file diff --git a/libs/core/security/src/lib/authentication/application/services/password/password.service.mock.ts b/libs/core/security/src/lib/password/application/services/password.service.mock.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/services/password/password.service.mock.ts rename to libs/core/security/src/lib/password/application/services/password.service.mock.ts diff --git a/libs/core/security/src/lib/authentication/application/services/password/password.service.spec.ts b/libs/core/security/src/lib/password/application/services/password.service.spec.ts similarity index 100% rename from libs/core/security/src/lib/authentication/application/services/password/password.service.spec.ts rename to libs/core/security/src/lib/password/application/services/password.service.spec.ts diff --git a/libs/core/security/src/lib/password/application/services/password.service.ts b/libs/core/security/src/lib/password/application/services/password.service.ts new file mode 100644 index 0000000..cb4dee1 --- /dev/null +++ b/libs/core/security/src/lib/password/application/services/password.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { PasswordHasher } from '../../domain/password-hasher.interface'; + +/** + * @class PasswordService + * + */ +@Injectable() +export class PasswordService { + constructor( + private readonly passwordHasher: PasswordHasher, + ) {} + + async hashPassword(password: string): Promise { + return this.passwordHasher.hash(password); + } + + async comparePassword(password: string, hash: string): Promise { + return this.passwordHasher.compare(password, hash); + } +} diff --git a/libs/core/security/src/lib/password/domain/index.ts b/libs/core/security/src/lib/password/domain/index.ts new file mode 100644 index 0000000..8e02bd0 --- /dev/null +++ b/libs/core/security/src/lib/password/domain/index.ts @@ -0,0 +1,3 @@ +export * from './value-objects/otpcode.value-object'; +export * from './password-hasher.interface'; +export * from './password-validation.decorators'; \ No newline at end of file diff --git a/libs/core/security/src/lib/password/domain/password-hasher.interface.ts b/libs/core/security/src/lib/password/domain/password-hasher.interface.ts new file mode 100644 index 0000000..102010e --- /dev/null +++ b/libs/core/security/src/lib/password/domain/password-hasher.interface.ts @@ -0,0 +1,4 @@ +export abstract class PasswordHasher { + abstract hash(password: string): Promise; + abstract compare(password: string, hashedPassword: string): Promise; +} \ No newline at end of file diff --git a/libs/core/security/src/lib/password/domain/password-validation.decorators.ts b/libs/core/security/src/lib/password/domain/password-validation.decorators.ts new file mode 100644 index 0000000..a0f25e4 --- /dev/null +++ b/libs/core/security/src/lib/password/domain/password-validation.decorators.ts @@ -0,0 +1,54 @@ +import { + registerDecorator, + ValidationOptions, + ValidationArguments, +} from 'class-validator'; + +export function IsStrongPassword(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isStrongPassword', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const regex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + return typeof value === 'string' && regex.test(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one number and one special character`; + }, + }, + }); + }; +} + +export function PasswordsMatch( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'passwordsMatch', + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as any)[ + relatedPropertyName + ]; + return value === relatedValue; + }, + defaultMessage(args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + return `${propertyName} must match ${relatedPropertyName}`; + }, + }, + }); + }; +} diff --git a/libs/core/security/src/lib/authentication/domain/value-objects/otpcode.value-object.ts b/libs/core/security/src/lib/password/domain/value-objects/otpcode.value-object.ts similarity index 60% rename from libs/core/security/src/lib/authentication/domain/value-objects/otpcode.value-object.ts rename to libs/core/security/src/lib/password/domain/value-objects/otpcode.value-object.ts index bf7a57c..565c81a 100644 --- a/libs/core/security/src/lib/authentication/domain/value-objects/otpcode.value-object.ts +++ b/libs/core/security/src/lib/password/domain/value-objects/otpcode.value-object.ts @@ -1,9 +1,9 @@ import { ValueObject } from '@goran/common'; -import otp from "otp-generator"; +import * as otp from "otp-generator"; export class OtpCodeVO extends ValueObject { static create() { - return new OtpCodeVO({ value: otp.generator(6, { upperCaseAlphabets: false, specialChars: false }) }); + return new OtpCodeVO({ value: otp.generate(6, { upperCaseAlphabets: false, specialChars: false }) }); } protected validate({ value }: { value: string }): void { diff --git a/libs/core/security/src/lib/password/index.ts b/libs/core/security/src/lib/password/index.ts new file mode 100644 index 0000000..74eb56b --- /dev/null +++ b/libs/core/security/src/lib/password/index.ts @@ -0,0 +1,3 @@ +export * from './domain'; +export * from './application'; +export * from './password.module'; diff --git a/libs/core/security/src/lib/password/infrastructure/bcrypt-password-hasher.ts b/libs/core/security/src/lib/password/infrastructure/bcrypt-password-hasher.ts new file mode 100644 index 0000000..7a2390b --- /dev/null +++ b/libs/core/security/src/lib/password/infrastructure/bcrypt-password-hasher.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { PasswordHasher } from '../domain/password-hasher.interface'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class BcryptPasswordHasher implements PasswordHasher { + async hash(password: string): Promise { + const saltRounds = 10; + return bcrypt.hash(password, saltRounds); + } + + async compare(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); + } +} \ No newline at end of file diff --git a/libs/core/security/src/lib/password/infrastructure/index.ts b/libs/core/security/src/lib/password/infrastructure/index.ts new file mode 100644 index 0000000..d074843 --- /dev/null +++ b/libs/core/security/src/lib/password/infrastructure/index.ts @@ -0,0 +1 @@ +export * from './bcrypt-password-hasher'; diff --git a/libs/core/security/src/lib/password/password.module.ts b/libs/core/security/src/lib/password/password.module.ts new file mode 100644 index 0000000..0c1e306 --- /dev/null +++ b/libs/core/security/src/lib/password/password.module.ts @@ -0,0 +1,17 @@ +import { Global, Module } from '@nestjs/common'; +import { BcryptPasswordHasher } from './infrastructure'; +import { PasswordService } from './application'; +import { PasswordHasher } from './domain'; + +@Global() +@Module({ + providers: [ + PasswordService, + { + provide: PasswordHasher, + useClass: BcryptPasswordHasher, + }, + ], + exports: [PasswordService], +}) +export class PasswordModule { } diff --git a/libs/core/security/src/lib/security-options.interface.ts b/libs/core/security/src/lib/security-options.interface.ts new file mode 100644 index 0000000..eab91f9 --- /dev/null +++ b/libs/core/security/src/lib/security-options.interface.ts @@ -0,0 +1,7 @@ +export interface SecurityOptions { + expiresIn: string; + refreshIn: string; + bcryptSalt: string; + jwtRefreshSecret: string; + jwtAccessSecret: string; +} diff --git a/libs/core/security/src/lib/session-management/index.ts b/libs/core/security/src/lib/session-management/index.ts deleted file mode 100644 index bba3f32..0000000 --- a/libs/core/security/src/lib/session-management/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./session-management.module" diff --git a/libs/core/security/src/lib/session-management/session-management.module.ts b/libs/core/security/src/lib/session-management/session-management.module.ts deleted file mode 100644 index 17889d5..0000000 --- a/libs/core/security/src/lib/session-management/session-management.module.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Module } from '@nestjs/common'; - -@Module({}) -export class SessionManagementModule {} diff --git a/libs/core/security/src/lib/sessions/application/factories/index.ts b/libs/core/security/src/lib/sessions/application/factories/index.ts new file mode 100644 index 0000000..5f317ec --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/factories/index.ts @@ -0,0 +1 @@ +export * from './session-token.factory'; \ No newline at end of file diff --git a/libs/core/security/src/lib/authentication/application/factories/authentication-token.factory.ts b/libs/core/security/src/lib/sessions/application/factories/session-token.factory.ts similarity index 70% rename from libs/core/security/src/lib/authentication/application/factories/authentication-token.factory.ts rename to libs/core/security/src/lib/sessions/application/factories/session-token.factory.ts index b260212..7cde8eb 100644 --- a/libs/core/security/src/lib/authentication/application/factories/authentication-token.factory.ts +++ b/libs/core/security/src/lib/sessions/application/factories/session-token.factory.ts @@ -1,18 +1,18 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; -import { TokenValueObject } from '../../domain'; +import { TokenValueObject } from '../../../tokens/domain'; import { CONFIG_APP } from '@goran/config'; -import { JwtPayloadSign } from '../jwt-payloads'; +import { JwtPayloadSign } from '../../domain/jwt-payloads'; @Injectable() -export class AuthenticationTokenFactory { +export class SessionTokenFactory { constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService ) { } - private readonly logger = new Logger(AuthenticationTokenFactory.name); + private readonly logger = new Logger(SessionTokenFactory.name); private readonly refreshSecretKey = this.configService.get( CONFIG_APP.JWT_REFRESH_SECRET @@ -21,10 +21,17 @@ export class AuthenticationTokenFactory { CONFIG_APP.SECURITY_REFRESH_IN ); + generateAccessTokenForRefreshToken( + refreshToken: string, + payload: JwtPayloadSign + ): TokenValueObject { + return new TokenValueObject({ + accessToken: this.generateAccessToken(payload), + refreshToken, + }); + } + generateTokens(payload: JwtPayloadSign): TokenValueObject { - this.logger.verbose( - `Generated access and refresh token for ${payload.userId} user` - ); return new TokenValueObject({ accessToken: this.generateAccessToken(payload), refreshToken: this.generateRefreshToken(payload), diff --git a/libs/core/security/src/lib/sessions/application/index.ts b/libs/core/security/src/lib/sessions/application/index.ts new file mode 100644 index 0000000..315fef7 --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/index.ts @@ -0,0 +1,5 @@ +export * from './ports'; +export * from './factories'; +export * from './sessions.model'; +export * from './sessions.mapper'; +export * from './services'; diff --git a/libs/core/security/src/lib/sessions/application/ports/index.ts b/libs/core/security/src/lib/sessions/application/ports/index.ts new file mode 100644 index 0000000..ba9212e --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/ports/index.ts @@ -0,0 +1 @@ +export * from './sessions.repository'; \ No newline at end of file diff --git a/libs/core/security/src/lib/sessions/application/ports/sessions.repository.ts b/libs/core/security/src/lib/sessions/application/ports/sessions.repository.ts new file mode 100644 index 0000000..41c545f --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/ports/sessions.repository.ts @@ -0,0 +1,29 @@ +import { SessionEntity, SessionStatus } from '../../domain'; +import { SessionModel } from '../sessions.model'; +import { ExceptionBase } from '@goran/common'; +import { Result, Option } from 'oxide.ts'; + +export abstract class SessionsWriteModelRepository { + abstract create( + session: SessionEntity + ): Promise>; + + abstract deleteByRefreshToken( + token: string + ): Promise>; + + abstract revokeByRefreshToken( + token: string + ): Promise>; + + abstract delete(sessionId: string): Promise>; +} + +export abstract class SessionsReadModelRepository { + abstract findByRefreshToken(token: string): Promise>; + abstract findOneById(sessionId: string): Promise>; + abstract findByUserId( + userId: string, + sessionsStatus?: SessionStatus + ): Promise; +} diff --git a/libs/core/security/src/lib/sessions/application/services/index.ts b/libs/core/security/src/lib/sessions/application/services/index.ts new file mode 100644 index 0000000..73155be --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/services/index.ts @@ -0,0 +1 @@ +export * from './sessions.service'; diff --git a/libs/core/security/src/lib/sessions/application/services/sessions.service.ts b/libs/core/security/src/lib/sessions/application/services/sessions.service.ts new file mode 100644 index 0000000..aa4da5a --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/services/sessions.service.ts @@ -0,0 +1,205 @@ +import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { UsersService } from '@goran/users'; +import { + SessionsReadModelRepository, + SessionsWriteModelRepository, +} from '../ports'; +import { SessionEntity, SessionStatus } from '../../domain'; +import { UserEntity } from '@goran/users'; +import { SessionTokenFactory } from '../factories'; +import { + InvalidTokenError, + TokensService, + TokenValueObject, +} from '../../../tokens'; +import { AggregateID, ExceptionBase, Optional } from '@goran/common'; +import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; +import { None, Ok, Option, Result, Err, match } from 'oxide.ts'; +import { SessionModel } from '../sessions.model'; +import { + SessionNotFoundError, + SessionRevokeFailedError, + SessionDeletionFailedError, +} from '../../domain/errors'; +import { SessionRevokedError } from '../../domain/errors/session-revoked.error'; + +@Injectable() +export class SessionsService { + constructor( + private readonly jwtService: JwtService, + private readonly usersService: UsersService, + private readonly sessionsReadRepository: SessionsReadModelRepository, + private readonly sessionsWriteRepository: SessionsWriteModelRepository, + private readonly sessionTokenFactory: SessionTokenFactory, + @Inject(CACHE_MANAGER) private cacheManager: Cache, + @Inject('REFRESH_IN') private readonly refreshIn: number, + private readonly tokensService: TokensService + ) {} + + async createSession( + user: UserEntity, + ip: string, + location?: Optional, + device?: Optional + ): Promise> { + const tokens = this.sessionTokenFactory.generateTokens({ + userId: user.id, + }); + const session = SessionEntity.create({ + userId: user.id, + location, + device, + ip, + refreshToken: tokens.refreshToken, + }); + + const createResult = await this.sessionsWriteRepository.create(session); + + return match(createResult, { + Ok: () => Ok([tokens, session]), + Err: (err) => + Err(err) as Result< + [TokenValueObject, SessionEntity], + ExceptionBase + >, + }); + } + + /** + * @description Refresh token validation + * @param token - Refresh token + */ + async validateRefreshToken(token: string): Promise> { + try { + const payload = this.jwtService.verify(token); + const session = + await this.sessionsReadRepository.findByRefreshToken(token); + if (!session) throw new UnauthorizedException('Invalid session'); + return await this.usersService.findOneByIdDomain(payload.userId); + } catch (e) { + return None; + } + } + + async isRefreshTokenRevoked( + token: string + ): Promise> { + const revokedToken: SessionStatus | undefined = + await this.cacheManager.get(token); + + if (revokedToken === 'revoked') return Ok(true); + + const sessionOption = + await this.sessionsReadRepository.findByRefreshToken(token); + + if (sessionOption.isNone()) return Err(new SessionNotFoundError()); + if (sessionOption.unwrap().status === 'revoked') return Ok(true); + + return Ok(false); + } + + async refreshAccessTokenForSession( + userId: AggregateID, + refreshToken: string + ): Promise> { + const isRefreshTokenValid = + await this.tokensService.refreshTokenIsValid(refreshToken); + + if (!isRefreshTokenValid) { + return Err(new InvalidTokenError()); + } + const isRevoked = await this.isRefreshTokenRevoked(refreshToken); + + return match(isRevoked, { + Err: (err) => Err(err) as Result, + Ok: (result) => + result + ? Err(new SessionRevokedError()) + : Ok( + this.sessionTokenFactory.generateAccessTokenForRefreshToken( + refreshToken, + { userId } + ) + ), + }); + } + + async revokeSessionByRefreshToken( + refreshToken: string + ): Promise> { + try { + await this.cacheManager.set( + refreshToken, + 'revoked', + this.refreshIn + ); + return await this.sessionsWriteRepository.revokeByRefreshToken( + refreshToken + ); + } catch (error) { + return Err(new SessionRevokeFailedError()); + } + } + + async revokeSession( + sessionId: string + ): Promise> { + const sessionOption = await this.sessionsReadRepository.findOneById( + sessionId + ); + + if (sessionOption.isNone()) return Err(new SessionNotFoundError()); + + try { + const session = sessionOption.unwrap(); + await this.cacheManager.set( + session.refreshToken, + 'revoked', + this.refreshIn + ); + return await this.sessionsWriteRepository.revokeByRefreshToken( + session.refreshToken + ); + } catch (error) { + return Err(new SessionRevokeFailedError()); + } + } + + async deleteSession( + sessionId: string + ): Promise> { + try { + const result = await this.sessionsWriteRepository.delete(sessionId); + if (result.isErr()) return Err(new SessionNotFoundError()); + return Ok(true); + } catch (error) { + return Err(new SessionDeletionFailedError()); + } + } + + async getSessionByToken(token: string): Promise> { + return await this.sessionsReadRepository.findByRefreshToken(token); + } + + /** + * @param {AggregateId} userId - User Id + * @param {SessionStatus} sessionsStatus - Filtering by status + * @returns The list of sessions with associating session status + */ + async getSessionsByUser( + userId: AggregateID, + sessionsStatus?: SessionStatus + ): Promise> { + const sessions = await this.sessionsReadRepository.findByUserId( + userId, + sessionsStatus + ); + + if (!sessions) { + return Err(new SessionNotFoundError()); + } + + return Ok(sessions); + } +} diff --git a/libs/core/security/src/lib/sessions/application/sessions.mapper.ts b/libs/core/security/src/lib/sessions/application/sessions.mapper.ts new file mode 100644 index 0000000..c621326 --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/sessions.mapper.ts @@ -0,0 +1,41 @@ +import { Mapper } from '@goran/common'; +import { SessionEntity } from '../domain/entities/session/session.entity'; +import { SessionModel } from './sessions.model'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SessionMapper implements Mapper { + toPersistence(entity: SessionEntity): SessionModel { + const props = entity.getProps(); + return { + id: props.id, + userId: props.userId, + refreshToken: props.refreshToken, + ip: props.ip, + location: props.location ?? null, + device: props.device ?? null, + status: props.status, + expire: props.expire, + createdAt: props.createdAt, + updatedAt: props.updatedAt, + }; + } + + async toDomain(record: SessionModel): Promise { + const entity = new SessionEntity({ + id: record.id, + createdAt: new Date(record.createdAt), + updatedAt: new Date(record.updatedAt), + props: { + userId: record.userId, + refreshToken: record.refreshToken, + ip: record.ip, + location: record.location ?? undefined, + device: record.device ?? undefined, + status: record.status, + expire: new Date(record.expire), + }, + }); + return entity; + } +} diff --git a/libs/core/security/src/lib/sessions/application/sessions.model.ts b/libs/core/security/src/lib/sessions/application/sessions.model.ts new file mode 100644 index 0000000..6ae07be --- /dev/null +++ b/libs/core/security/src/lib/sessions/application/sessions.model.ts @@ -0,0 +1,24 @@ +import { AggregateID, Optional } from '@goran/common'; +import { z } from 'zod'; + +export const sessionSchema = z.object({ + id: z.string().ulid(), + createdAt: z.preprocess((val: any) => new Date(val), z.date()), + updatedAt: z.preprocess((val: any) => new Date(val), z.date()), + userId: z.string().ulid(), + refreshToken: z.string(), + ip: z.string(), + location: z.string().optional().nullable(), + device: z.string().optional().nullable(), + status: z.enum(['active', 'revoked']), + expire: z.preprocess((val: any) => new Date(val), z.date()), +}); + +export type SessionModel = z.TypeOf; +export type SessionDto = { + userId: string; + refreshToken: string; + ip: string; + location?: Optional; + device?: Optional; +}; diff --git a/libs/core/security/src/lib/sessions/domain/entities/index.ts b/libs/core/security/src/lib/sessions/domain/entities/index.ts new file mode 100644 index 0000000..1ae27f4 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/entities/index.ts @@ -0,0 +1 @@ +export * from './session'; diff --git a/libs/core/security/src/lib/sessions/domain/entities/session/index.ts b/libs/core/security/src/lib/sessions/domain/entities/session/index.ts new file mode 100644 index 0000000..30ade73 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/entities/session/index.ts @@ -0,0 +1,2 @@ +export * from './session.type'; +export * from './session.entity'; diff --git a/libs/core/security/src/lib/sessions/domain/entities/session/session.entity.ts b/libs/core/security/src/lib/sessions/domain/entities/session/session.entity.ts new file mode 100644 index 0000000..1d1f3f1 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/entities/session/session.entity.ts @@ -0,0 +1,29 @@ +import { AggregateRoot, AggregateID } from '@goran/common'; +import { CreateSessionProps, SessionProps } from './session.type'; +import { ulid } from 'ulid'; +import { addDays } from 'date-fns'; + +export class SessionEntity extends AggregateRoot { + protected readonly _id: AggregateID; + + static create(create: CreateSessionProps): SessionEntity { + const id = ulid(); + const props: SessionProps = { + ...create, + status: 'active', + expire: addDays(new Date(), 30), + }; + return new SessionEntity({ + id, + props, + createdAt: new Date(), + updatedAt: new Date(), + }); + } + + terminate(): void {} + + validate(): void { + return; + } +} diff --git a/libs/core/security/src/lib/sessions/domain/entities/session/session.type.ts b/libs/core/security/src/lib/sessions/domain/entities/session/session.type.ts new file mode 100644 index 0000000..db108b2 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/entities/session/session.type.ts @@ -0,0 +1,23 @@ +import { AggregateID, RequireOnlyOne, Optional } from '@goran/common'; + +export type SessionStatus = 'active' | 'revoked'; + +export interface SessionProps { + refreshToken: string; + ip: string; + device: Optional; + location: Optional; + status: SessionStatus; + expire: Date; + userId: AggregateID; +} + +export interface CreateSessionProps { + refreshToken: string; + ip: string; + device: Optional; + location: Optional; + userId: AggregateID; +} + +export type UpdateUserProps = RequireOnlyOne; diff --git a/libs/core/security/src/lib/sessions/domain/errors/index.ts b/libs/core/security/src/lib/sessions/domain/errors/index.ts new file mode 100644 index 0000000..60c7d16 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/errors/index.ts @@ -0,0 +1,5 @@ +export * from './session-creation-failed.error'; +export * from './session-deletion-failed.error'; +export * from './session-not-found.error'; +export * from './session-revoke-failed.error'; +export * from './session-revoked.error'; diff --git a/libs/core/security/src/lib/sessions/domain/errors/session-creation-failed.error.ts b/libs/core/security/src/lib/sessions/domain/errors/session-creation-failed.error.ts new file mode 100644 index 0000000..f376417 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/errors/session-creation-failed.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class SessionCreationFailedError extends ExceptionBase { + static readonly message = 'Failed to create session'; + + public readonly code = 'SESSION.CREATE_FAILED'; + + constructor(cause?: Error, metadata?: unknown) { + super(SessionCreationFailedError.message, cause, metadata); + } +} diff --git a/libs/core/security/src/lib/sessions/domain/errors/session-deletion-failed.error.ts b/libs/core/security/src/lib/sessions/domain/errors/session-deletion-failed.error.ts new file mode 100644 index 0000000..c304c01 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/errors/session-deletion-failed.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class SessionDeletionFailedError extends ExceptionBase { + static readonly message = 'Failed to delete session'; + + public readonly code = 'SESSION.DELETION_FAILED'; + + constructor(cause?: Error, metadata?: unknown) { + super(SessionDeletionFailedError.message, cause, metadata); + } +} diff --git a/libs/core/security/src/lib/sessions/domain/errors/session-not-found.error.ts b/libs/core/security/src/lib/sessions/domain/errors/session-not-found.error.ts new file mode 100644 index 0000000..c46a2e5 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/errors/session-not-found.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class SessionNotFoundError extends ExceptionBase { + static readonly message = 'Session not found'; + + public readonly code = 'SESSION.NOT_FOUND'; + + constructor(cause?: Error, metadata?: unknown) { + super(SessionNotFoundError.message, cause, metadata); + } +} diff --git a/libs/core/security/src/lib/sessions/domain/errors/session-revoke-failed.error.ts b/libs/core/security/src/lib/sessions/domain/errors/session-revoke-failed.error.ts new file mode 100644 index 0000000..fd5775f --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/errors/session-revoke-failed.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class SessionRevokeFailedError extends ExceptionBase { + static readonly message = 'Failed to revoke session'; + + public readonly code = 'SESSION.REVOKE_FAILED'; + + constructor(cause?: Error, metadata?: unknown) { + super(SessionRevokeFailedError.message, cause, metadata); + } +} diff --git a/libs/core/security/src/lib/sessions/domain/errors/session-revoked.error.ts b/libs/core/security/src/lib/sessions/domain/errors/session-revoked.error.ts new file mode 100644 index 0000000..a2b6c52 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/errors/session-revoked.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class SessionRevokedError extends ExceptionBase { + static readonly message = 'Session is revoked'; + + public readonly code = 'SESSION.REVOKED'; + + constructor(cause?: Error, metadata?: unknown) { + super(SessionRevokedError.message, cause, metadata); + } +} diff --git a/libs/core/security/src/lib/sessions/domain/index.ts b/libs/core/security/src/lib/sessions/domain/index.ts new file mode 100644 index 0000000..05e5bca --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/index.ts @@ -0,0 +1,3 @@ +export * from './jwt-payloads'; +export * from './entities'; +export * from './errors'; diff --git a/libs/core/security/src/lib/sessions/domain/jwt-payloads.ts b/libs/core/security/src/lib/sessions/domain/jwt-payloads.ts new file mode 100644 index 0000000..374afe1 --- /dev/null +++ b/libs/core/security/src/lib/sessions/domain/jwt-payloads.ts @@ -0,0 +1,9 @@ +import { AggregateID } from "@goran/common"; + +export interface JwtPayload { + userId: AggregateID; + issuedAt: number; + expiration: number; +} + +export type JwtPayloadSign = Omit; diff --git a/libs/core/security/src/lib/sessions/index.ts b/libs/core/security/src/lib/sessions/index.ts new file mode 100644 index 0000000..2e5ba9f --- /dev/null +++ b/libs/core/security/src/lib/sessions/index.ts @@ -0,0 +1,3 @@ +export * from './application'; +export * from './domain'; +export * from './sessions.module'; diff --git a/libs/core/security/src/lib/sessions/infrastructure/index.ts b/libs/core/security/src/lib/sessions/infrastructure/index.ts new file mode 100644 index 0000000..c4aca51 --- /dev/null +++ b/libs/core/security/src/lib/sessions/infrastructure/index.ts @@ -0,0 +1 @@ +export * from './persistence' \ No newline at end of file diff --git a/libs/core/security/src/lib/sessions/infrastructure/persistence/index.ts b/libs/core/security/src/lib/sessions/infrastructure/persistence/index.ts new file mode 100644 index 0000000..608c736 --- /dev/null +++ b/libs/core/security/src/lib/sessions/infrastructure/persistence/index.ts @@ -0,0 +1,2 @@ +export * from './sessions-write-postgres.repository' +export * from './postgres-read-sessions.repository' \ No newline at end of file diff --git a/libs/core/security/src/lib/sessions/infrastructure/persistence/postgres-read-sessions.repository.ts b/libs/core/security/src/lib/sessions/infrastructure/persistence/postgres-read-sessions.repository.ts new file mode 100644 index 0000000..c02d592 --- /dev/null +++ b/libs/core/security/src/lib/sessions/infrastructure/persistence/postgres-read-sessions.repository.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { SessionsReadModelRepository } from '../../application'; +import { and, eq } from 'drizzle-orm'; +import { SessionModel } from '../../application'; +import { + SessionsDataPgTable as SessionsTable, + DrizzleService, +} from '@goran/drizzle-data-access'; +import { None, Some, Option } from 'oxide.ts'; +import { SessionStatus } from '../../domain'; + +@Injectable() +export class SessionsReadModelRepositoryPostgres + implements SessionsReadModelRepository +{ + constructor(private readonly drizzleService: DrizzleService) {} + + async findByRefreshToken(token: string): Promise> { + const [session] = await this.drizzleService.db + .select() + .from(SessionsTable) + .where(eq(SessionsTable.refreshToken, token)); + return session ? Some(session) : None; + } + + async findByUserId( + userId: string, + sessionsStatus?: SessionStatus + ): Promise { + return await this.drizzleService.db + .select() + .from(SessionsTable) + .where( + and( + eq(SessionsTable.userId, userId), + sessionsStatus && eq(SessionsTable.status, sessionsStatus) + ) + ); + } + + async findOneById(sessionId: string): Promise> { + const [session] = await this.drizzleService.db + .select() + .from(SessionsTable) + .where(eq(SessionsTable.id, sessionId)); + return session ? Some(session) : None; + } +} diff --git a/libs/core/security/src/lib/sessions/infrastructure/persistence/sessions-write-postgres.repository.ts b/libs/core/security/src/lib/sessions/infrastructure/persistence/sessions-write-postgres.repository.ts new file mode 100644 index 0000000..1a97151 --- /dev/null +++ b/libs/core/security/src/lib/sessions/infrastructure/persistence/sessions-write-postgres.repository.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@nestjs/common'; +import { SessionsWriteModelRepository, SessionMapper } from '../../application'; +import { SessionEntity } from '../../domain'; +import { eq } from 'drizzle-orm'; +import { + SessionsDataPgTable as SessionsTable, + DrizzleService, +} from '@goran/drizzle-data-access'; +import { ExceptionBase } from '@goran/common'; +import { Err, Ok, Result } from 'oxide.ts'; +import { + SessionCreationFailedError, + SessionNotFoundError, + SessionRevokeFailedError, +} from '../../domain/errors'; + +@Injectable() +export class SessionsWriteModelRepositoryPostgres + implements SessionsWriteModelRepository { + constructor( + private readonly drizzleService: DrizzleService, + private readonly sessionMapper: SessionMapper + ) { } + + async create( + session: SessionEntity + ): Promise> { + try { + const persistenceModel = this.sessionMapper.toPersistence(session); + const [created] = await this.drizzleService.db + .insert(SessionsTable) + .values({ ...persistenceModel }) + .returning(); + return Ok(await this.sessionMapper.toDomain(created)); + } catch (error) { + return Err(new SessionCreationFailedError()); + } + } + + async revokeByRefreshToken( + token: string + ): Promise> { + try { + const [updatedSession] = await this.drizzleService.db + .update(SessionsTable) + .set({ + status: 'revoked', + expire: new Date(), + }) + .where(eq(SessionsTable.refreshToken, token)) + .returning(); + if (!updatedSession) { + return Err(new SessionRevokeFailedError()); + } + const sessionEntity = await this.sessionMapper.toDomain( + updatedSession + ); + return Ok(sessionEntity); + } catch (error) { + return Err(new SessionRevokeFailedError()); + } + } + + async revokeSession( + sessionId: string + ): Promise> { + try { + const [updatedSession] = await this.drizzleService.db + .update(SessionsTable) + .set({ + status: 'revoked', + expire: new Date(), + }) + .where(eq(SessionsTable.id, sessionId)) + .returning(); + if (!updatedSession) { + return Err(new SessionRevokeFailedError()); + } + const sessionEntity = await this.sessionMapper.toDomain( + updatedSession + ); + return Ok(sessionEntity); + } catch (error) { + return Err(new SessionRevokeFailedError()); + } + } + + async deleteByRefreshToken( + token: string + ): Promise> { + const result = await this.drizzleService.db + .delete(SessionsTable) + .where(eq(SessionsTable.refreshToken, token)) + .returning(); + if (result.length !== 0) return Ok(true); + else return Err(new SessionNotFoundError()); + } + + async delete(sessionId: string): Promise> { + const result = await this.drizzleService.db + .delete(SessionsTable) + .where(eq(SessionsTable.id, sessionId)) + .returning(); + if (result.length !== 0) return Ok(true); + else return Err(new SessionNotFoundError()); + } +} diff --git a/libs/core/security/src/lib/sessions/presenters/http/sessions/delete-session.response.ts b/libs/core/security/src/lib/sessions/presenters/http/sessions/delete-session.response.ts new file mode 100644 index 0000000..82dd7f6 --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/sessions/delete-session.response.ts @@ -0,0 +1,11 @@ +import { ResponseBase } from '@goran/common'; + +export class DeleteSessionSuccessResponse extends ResponseBase { + constructor() { + super({ + message: 'Session deleted successfully', + success: true, + code: 'SESSION_DELETED', + }); + } +} diff --git a/libs/core/security/src/lib/sessions/presenters/http/sessions/get-sessions.response.ts b/libs/core/security/src/lib/sessions/presenters/http/sessions/get-sessions.response.ts new file mode 100644 index 0000000..2feaa6a --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/sessions/get-sessions.response.ts @@ -0,0 +1,17 @@ +import { ResponseBase } from '@goran/common'; +import { SessionDto } from '../../../application'; + +interface SessionResponseProps { + sessions: SessionDto[]; +} + +export class GetSessionsSuccessResponse extends ResponseBase { + constructor(props: SessionResponseProps) { + super({ + data: props, + message: 'Sessions retrieved successfully', + success: true, + code: 'SESSIONS_RETRIEVED', + }); + } +} diff --git a/libs/core/security/src/lib/sessions/presenters/http/sessions/index.ts b/libs/core/security/src/lib/sessions/presenters/http/sessions/index.ts new file mode 100644 index 0000000..138e0b8 --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/sessions/index.ts @@ -0,0 +1 @@ +export * from './sessions.controller'; diff --git a/libs/core/security/src/lib/sessions/presenters/http/sessions/revoked-session.response.ts b/libs/core/security/src/lib/sessions/presenters/http/sessions/revoked-session.response.ts new file mode 100644 index 0000000..9ddcf11 --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/sessions/revoked-session.response.ts @@ -0,0 +1,11 @@ +import { ResponseBase } from '@goran/common'; + +export class RevokedSessionSuccessResponse extends ResponseBase { + constructor() { + super({ + message: 'Session revoked successfully', + success: true, + code: 'SESSION_REVOKED', + }); + } +} diff --git a/libs/core/security/src/lib/sessions/presenters/http/sessions/sessions.controller.ts b/libs/core/security/src/lib/sessions/presenters/http/sessions/sessions.controller.ts new file mode 100644 index 0000000..8956274 --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/sessions/sessions.controller.ts @@ -0,0 +1,83 @@ +import { + Controller, + Delete, + Get, + InternalServerErrorException, + Param, + Post, + Query, + UnauthorizedException, + UseGuards, +} from '@nestjs/common'; +import { SessionStatus } from '../../../domain'; +import { SessionDto, SessionsService } from '../../../application'; +import { JwtAuthGuard } from '../../../../authentication'; +import { ApiTags } from '@nestjs/swagger'; +import { match, Result } from 'oxide.ts'; +import { GetSessionsSuccessResponse } from './get-sessions.response'; +import { DeleteSessionSuccessResponse } from './delete-session.response'; +import { CurrentUser, UserEntity } from '@goran/users'; +import { RevokedSessionSuccessResponse } from './revoked-session.response'; +import { ExceptionBase } from '@goran/common'; + +@UseGuards(JwtAuthGuard) +@Controller() +export class SessionsController { + constructor(private readonly sessionsService: SessionsService) { } + + @ApiTags('users', 'sessions') + @Get('/users/:id/sessions') + async getSessions( + @Param('id') userId: string, + @CurrentUser() user: UserEntity, + @Query('status') status?: SessionStatus + ) { + if (user.id !== userId) throw new UnauthorizedException(); + const sessions = await this.sessionsService.getSessionsByUser(userId, status); + return match(sessions, { + Ok: (sessions: SessionDto[]) => new GetSessionsSuccessResponse({ sessions }), + Err: (error: Error) => { + throw error; + } + }) + } + + @ApiTags('users', 'sessions') + @Get('/@me/sessions') + async getUsersSessions( + @CurrentUser() user: UserEntity, + @Query('status') status?: SessionStatus + ) { + const sessions = await this.sessionsService.getSessionsByUser(user.id, status); + return match(sessions, { + Ok: (sessions: SessionDto[]) => new GetSessionsSuccessResponse({ sessions }), + Err: (error: Error) => { + throw error; + } + }) + } + + @ApiTags('sessions') + @Post('/sessions/:id/revoke') + async revokeSession(@Param('id') sessionId: string) { + const result = await this.sessionsService.revokeSession(sessionId); + return match(result, { + Ok: () => new RevokedSessionSuccessResponse(), + Err: (error: Error) => { + throw error; + } + }); + } + + @ApiTags('sessions') + @Delete('/sessions/:id') + async deleteSession(@Param('id') sessionId: string) { + const result: Result = await this.sessionsService.deleteSession(sessionId); + return match(result, { + Ok: () => new DeleteSessionSuccessResponse(), + Err: (error: ExceptionBase) => { + throw new InternalServerErrorException(error); + } + }); + } +} diff --git a/libs/core/security/src/lib/sessions/presenters/http/tokens/index.ts b/libs/core/security/src/lib/sessions/presenters/http/tokens/index.ts new file mode 100644 index 0000000..4c503b0 --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/tokens/index.ts @@ -0,0 +1 @@ +export * from './refetch-token.controller'; diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication-token/authentication-token.controller.spec.ts b/libs/core/security/src/lib/sessions/presenters/http/tokens/refetch-token.controller.spec.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/authentication-token/authentication-token.controller.spec.ts rename to libs/core/security/src/lib/sessions/presenters/http/tokens/refetch-token.controller.spec.ts diff --git a/libs/core/security/src/lib/sessions/presenters/http/tokens/refetch-token.controller.ts b/libs/core/security/src/lib/sessions/presenters/http/tokens/refetch-token.controller.ts new file mode 100644 index 0000000..8efd1f2 --- /dev/null +++ b/libs/core/security/src/lib/sessions/presenters/http/tokens/refetch-token.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, Post, Req } from '@nestjs/common'; +import { RefreshTokenInput } from './refresh-token.dto'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '@goran/users'; +import { Request } from 'express'; +import { InvalidTokenError } from '@goran/security'; +import { SessionsService } from '../../../application'; + +@ApiTags('auth') +@Controller('auth/token/refresh') +export class RefreshTokenController { + constructor(private readonly sessionsService: SessionsService) {} + + @ApiOkResponse() + @Post() + async refreshToken( + @Req() req: Request, + @Body() refershTokenInput: RefreshTokenInput, + @CurrentUser() userId: string + ) { + const refreshToken = + refershTokenInput.token || req.cookies.refreshToken; + if (!refreshToken) { + throw new InvalidTokenError(); + } + const newToken = + await this.sessionsService.refreshAccessTokenForSession( + userId, + refreshToken + ); + return { accessToken: newToken }; + } +} diff --git a/libs/core/security/src/lib/authentication/presenters/http/authentication-token/refresh-token.dto.ts b/libs/core/security/src/lib/sessions/presenters/http/tokens/refresh-token.dto.ts similarity index 100% rename from libs/core/security/src/lib/authentication/presenters/http/authentication-token/refresh-token.dto.ts rename to libs/core/security/src/lib/sessions/presenters/http/tokens/refresh-token.dto.ts diff --git a/libs/core/security/src/lib/authentication/infrastructure/index.ts b/libs/core/security/src/lib/sessions/presenters/index.ts similarity index 100% rename from libs/core/security/src/lib/authentication/infrastructure/index.ts rename to libs/core/security/src/lib/sessions/presenters/index.ts diff --git a/libs/core/security/src/lib/sessions/sessions.module.ts b/libs/core/security/src/lib/sessions/sessions.module.ts new file mode 100644 index 0000000..58da0d9 --- /dev/null +++ b/libs/core/security/src/lib/sessions/sessions.module.ts @@ -0,0 +1,50 @@ +import { DynamicModule } from '@nestjs/common'; +import { TokensModule } from '../tokens'; +import { + SessionsService, + SessionTokenFactory, + SessionMapper, + SessionsWriteModelRepository, + SessionsReadModelRepository, +} from './application'; +import { + SessionsReadModelRepositoryPostgres, + SessionsWriteModelRepositoryPostgres, +} from './infrastructure'; +import { RefreshTokenController } from './presenters/http/tokens'; +import { JwtAuthGuard } from '../authentication'; + +export class SessionsModule { + static register(options: { refreshIn: string }): DynamicModule { + return { + module: SessionsModule, + global: true, + providers: [ + { + provide: SessionsWriteModelRepository, + useClass: SessionsWriteModelRepositoryPostgres, + }, + { + provide: SessionsReadModelRepository, + useClass: SessionsReadModelRepositoryPostgres, + }, + SessionMapper, + SessionsService, + SessionTokenFactory, + JwtAuthGuard, + { + provide: 'REFRESH_IN', + useValue: options.refreshIn, + }, + ], + imports: [TokensModule], + exports: [ + JwtAuthGuard, + SessionMapper, + SessionsService, + SessionTokenFactory, + ], + controllers: [RefreshTokenController], + }; + } +} diff --git a/libs/core/security/src/lib/tokens/application/index.ts b/libs/core/security/src/lib/tokens/application/index.ts new file mode 100644 index 0000000..f256c75 --- /dev/null +++ b/libs/core/security/src/lib/tokens/application/index.ts @@ -0,0 +1 @@ +export * from './tokens.service'; diff --git a/libs/core/security/src/lib/tokens/application/tokens.service.ts b/libs/core/security/src/lib/tokens/application/tokens.service.ts new file mode 100644 index 0000000..39e7759 --- /dev/null +++ b/libs/core/security/src/lib/tokens/application/tokens.service.ts @@ -0,0 +1,37 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { UsersService } from '@goran/users'; +import { Request } from 'express'; + +@Injectable() +export class TokensService { + constructor( + private readonly jwtService: JwtService, + private readonly usersService: UsersService + ) { } + + async getUserFromToken(token: string) { + const id = this.jwtService.decode(token)['userId']; + const user = await this.usersService.findOneById(id); + return user; + } + + async refreshTokenIsValid(refreshToken: string): Promise { + try { + const decodedRefreshToken = await this.jwtService.verify( + refreshToken + ); + return !!decodedRefreshToken; + } catch (error) { + return false; + } + } + + extractTokenFromRequest(req: Request) { + const token = req.headers?.authorization?.split(' ')[1]; + if (!token) { + throw new UnauthorizedException('Token must be provided'); + } + return token; + } +} diff --git a/libs/core/security/src/lib/tokens/domain/errors/index.ts b/libs/core/security/src/lib/tokens/domain/errors/index.ts new file mode 100644 index 0000000..3dbc1f8 --- /dev/null +++ b/libs/core/security/src/lib/tokens/domain/errors/index.ts @@ -0,0 +1 @@ +export * from './invalid-token.error'; \ No newline at end of file diff --git a/libs/core/security/src/lib/tokens/domain/errors/invalid-token.error.ts b/libs/core/security/src/lib/tokens/domain/errors/invalid-token.error.ts new file mode 100644 index 0000000..cd7c7a2 --- /dev/null +++ b/libs/core/security/src/lib/tokens/domain/errors/invalid-token.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class InvalidTokenError extends ExceptionBase { + static readonly message = 'Invalid Token provided'; + + public readonly code = 'TOKEN.INVALID'; + + constructor(cause?: Error, metadata?: unknown) { + super(InvalidTokenError.message, cause, metadata); + } +} diff --git a/libs/core/security/src/lib/tokens/domain/index.ts b/libs/core/security/src/lib/tokens/domain/index.ts new file mode 100644 index 0000000..e94c700 --- /dev/null +++ b/libs/core/security/src/lib/tokens/domain/index.ts @@ -0,0 +1,2 @@ +export * from './errors'; +export * from './value-objects'; \ No newline at end of file diff --git a/libs/core/security/src/lib/tokens/domain/value-objects/index.ts b/libs/core/security/src/lib/tokens/domain/value-objects/index.ts new file mode 100644 index 0000000..879fcfd --- /dev/null +++ b/libs/core/security/src/lib/tokens/domain/value-objects/index.ts @@ -0,0 +1 @@ +export * from './token.value-object'; diff --git a/libs/core/security/src/lib/tokens/domain/value-objects/token.value-object.ts b/libs/core/security/src/lib/tokens/domain/value-objects/token.value-object.ts new file mode 100644 index 0000000..f7a54d0 --- /dev/null +++ b/libs/core/security/src/lib/tokens/domain/value-objects/token.value-object.ts @@ -0,0 +1,26 @@ +import { ArgumentInvalidException, Guard, ValueObject } from '@goran/common'; + +export interface TokenProps { + accessToken: string; + refreshToken: string; +} + +export class TokenValueObject extends ValueObject { + get accessToken() { + return this.props.accessToken; + } + + get refreshToken() { + return this.props.refreshToken; + } + + protected override validate(props: TokenProps): void { + if ( + Guard.isEmpty(props.accessToken) || + Guard.isEmpty(props.refreshToken) + ) + throw new ArgumentInvalidException( + 'Neither access token and refresh token can be empty' + ); + } +} diff --git a/libs/core/security/src/lib/tokens/index.ts b/libs/core/security/src/lib/tokens/index.ts new file mode 100644 index 0000000..28390ef --- /dev/null +++ b/libs/core/security/src/lib/tokens/index.ts @@ -0,0 +1,3 @@ +export * from "./application"; +export * from "./domain"; +export * from "./tokens.module"; \ No newline at end of file diff --git a/libs/core/security/src/lib/tokens/tokens.module.ts b/libs/core/security/src/lib/tokens/tokens.module.ts new file mode 100644 index 0000000..ba3b3e3 --- /dev/null +++ b/libs/core/security/src/lib/tokens/tokens.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; +import { TokensService } from './application/tokens.service'; + +@Global() +@Module({ + imports: [], + providers: [TokensService], + exports: [TokensService], +}) +export class TokensModule { } diff --git a/libs/core/users/.eslintrc.json b/libs/core/users/.eslintrc.json new file mode 100644 index 0000000..9761c56 --- /dev/null +++ b/libs/core/users/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/domain/users/README.md b/libs/core/users/README.md similarity index 100% rename from libs/domain/users/README.md rename to libs/core/users/README.md diff --git a/libs/domain/users/jest.config.ts b/libs/core/users/jest.config.ts similarity index 53% rename from libs/domain/users/jest.config.ts rename to libs/core/users/jest.config.ts index 9ac9ead..41f1bd6 100644 --- a/libs/domain/users/jest.config.ts +++ b/libs/core/users/jest.config.ts @@ -4,8 +4,11 @@ export default { preset: '../../../jest.preset.js', testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]s$': [ + 'ts-jest', + { tsconfig: '/tsconfig.spec.json' }, + ], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../../coverage/libs/domain/users', + coverageDirectory: '../../../coverage/libs/core/users', }; diff --git a/libs/core/users/project.json b/libs/core/users/project.json new file mode 100644 index 0000000..4ceb2a8 --- /dev/null +++ b/libs/core/users/project.json @@ -0,0 +1,16 @@ +{ + "name": "users", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/core/users/src", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "options": { + "jestConfig": "libs/core/users/jest.config.ts", + "passWithNoTests": true + } + } + } +} diff --git a/libs/domain/users/src/application/commands/change-user-email/change-user-email.command-handler.ts b/libs/core/users/src/application/commands/change-user-email/change-user-email.command-handler.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-email/change-user-email.command-handler.ts rename to libs/core/users/src/application/commands/change-user-email/change-user-email.command-handler.ts diff --git a/libs/core/users/src/application/commands/change-user-email/change-user-email.command.ts b/libs/core/users/src/application/commands/change-user-email/change-user-email.command.ts new file mode 100644 index 0000000..1f52bd0 --- /dev/null +++ b/libs/core/users/src/application/commands/change-user-email/change-user-email.command.ts @@ -0,0 +1,13 @@ +import { Command, CommandProps } from '@goran/common'; +import { UserEntity } from '@goran/users'; + +export class ChangeUserEmailCommand extends Command { + readonly newEmail: string; + readonly user: UserEntity; + + constructor(props: CommandProps) { + super(props); + this.newEmail = props.newEmail; + this.user = props.user; + } +} diff --git a/libs/core/security/src/lib/authentication/infrastructure/persistence/index.ts b/libs/core/users/src/application/commands/change-user-email/change-user-email.test.ts similarity index 100% rename from libs/core/security/src/lib/authentication/infrastructure/persistence/index.ts rename to libs/core/users/src/application/commands/change-user-email/change-user-email.test.ts diff --git a/libs/domain/users/src/application/commands/change-user-email/index.ts b/libs/core/users/src/application/commands/change-user-email/index.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-email/index.ts rename to libs/core/users/src/application/commands/change-user-email/index.ts diff --git a/libs/domain/users/src/application/commands/change-user-password/change-user-password.command-handler.ts b/libs/core/users/src/application/commands/change-user-password/change-user-password.command-handler.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-password/change-user-password.command-handler.ts rename to libs/core/users/src/application/commands/change-user-password/change-user-password.command-handler.ts diff --git a/libs/domain/users/src/application/commands/change-user-password/change-user-password.command.ts b/libs/core/users/src/application/commands/change-user-password/change-user-password.command.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-password/change-user-password.command.ts rename to libs/core/users/src/application/commands/change-user-password/change-user-password.command.ts diff --git a/libs/domain/users/src/domain/rules/index.ts b/libs/core/users/src/application/commands/change-user-password/change-user-password.test.ts similarity index 100% rename from libs/domain/users/src/domain/rules/index.ts rename to libs/core/users/src/application/commands/change-user-password/change-user-password.test.ts diff --git a/libs/domain/users/src/application/commands/change-user-password/index.ts b/libs/core/users/src/application/commands/change-user-password/index.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-password/index.ts rename to libs/core/users/src/application/commands/change-user-password/index.ts diff --git a/libs/domain/users/src/application/commands/change-user-username/change-user-username.command-handler.ts b/libs/core/users/src/application/commands/change-user-username/change-user-username.command-handler.ts similarity index 72% rename from libs/domain/users/src/application/commands/change-user-username/change-user-username.command-handler.ts rename to libs/core/users/src/application/commands/change-user-username/change-user-username.command-handler.ts index 294ba47..7010d11 100644 --- a/libs/domain/users/src/application/commands/change-user-username/change-user-username.command-handler.ts +++ b/libs/core/users/src/application/commands/change-user-username/change-user-username.command-handler.ts @@ -5,14 +5,17 @@ import { WriteModelUsersRepository } from '../../ports'; @CommandHandler(ChangeUserUsernameCommand) export class ChangeUserUsernameCommandHandler - implements ICommandHandler { + implements ICommandHandler +{ private readonly logger = new Logger(ChangeUserUsernameCommandHandler.name); - constructor(private readonly userRepo: WriteModelUsersRepository) { } + constructor(private readonly userRepo: WriteModelUsersRepository) {} async execute(command: ChangeUserUsernameCommand) { this.logger.verbose( - `Processing user specifics with ${command.user.getProps().email} email` + `Processing user specifics with ${ + command.user.getProps().email + } email` ); this.logger.debug(`User specifics: ${JSON.stringify(command)}`); const dbResult = await this.userRepo.updateUsername( @@ -22,12 +25,14 @@ export class ChangeUserUsernameCommandHandler if (dbResult.isSome()) this.logger.debug( - `User's username with ${command.user.getProps().email + `User's username with ${ + command.user.getProps().email } email is updated.` ); else this.logger.debug( - `Failed to update user's username with ${command.user.getProps().email + `Failed to update user's username with ${ + command.user.getProps().email } email` ); diff --git a/libs/domain/users/src/application/commands/change-user-username/change-user-username.command.ts b/libs/core/users/src/application/commands/change-user-username/change-user-username.command.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-username/change-user-username.command.ts rename to libs/core/users/src/application/commands/change-user-username/change-user-username.command.ts diff --git a/libs/core/users/src/application/commands/change-user-username/change-user-username.test.ts b/libs/core/users/src/application/commands/change-user-username/change-user-username.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/domain/users/src/application/commands/change-user-username/index.ts b/libs/core/users/src/application/commands/change-user-username/index.ts similarity index 100% rename from libs/domain/users/src/application/commands/change-user-username/index.ts rename to libs/core/users/src/application/commands/change-user-username/index.ts diff --git a/libs/domain/users/src/application/commands/create-user/create-user.command-handler.ts b/libs/core/users/src/application/commands/create-user/create-user.command-handler.ts similarity index 67% rename from libs/domain/users/src/application/commands/create-user/create-user.command-handler.ts rename to libs/core/users/src/application/commands/create-user/create-user.command-handler.ts index 6356449..30fabe3 100644 --- a/libs/domain/users/src/application/commands/create-user/create-user.command-handler.ts +++ b/libs/core/users/src/application/commands/create-user/create-user.command-handler.ts @@ -1,21 +1,16 @@ import { Logger } from '@nestjs/common'; import { CreateUserCommand } from './create-user.command'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { - WriteModelUsersRepository, -} from '../../ports'; -import { - UserEntity, -} from '../../../domain/entities'; +import { WriteModelUsersRepository } from '../../ports'; +import { UserEntity } from '../../../domain/entities'; @CommandHandler(CreateUserCommand) export class CreateUserCommandHandler - implements ICommandHandler { + implements ICommandHandler +{ private readonly logger = new Logger(CreateUserCommandHandler.name); - constructor( - private readonly userRepo: WriteModelUsersRepository, - ) { } + constructor(private readonly userRepo: WriteModelUsersRepository) {} async execute(command: CreateUserCommand) { this.logger.debug( @@ -28,7 +23,9 @@ export class CreateUserCommandHandler const repoResult = await this.userRepo.insertOne(user); if (repoResult.isOk()) - this.logger.verbose(`User created with ${user.getProps().email} email`); + this.logger.verbose( + `User created with ${user.getProps().email} email` + ); return repoResult; } diff --git a/libs/domain/users/src/application/commands/create-user/create-user.command.ts b/libs/core/users/src/application/commands/create-user/create-user.command.ts similarity index 100% rename from libs/domain/users/src/application/commands/create-user/create-user.command.ts rename to libs/core/users/src/application/commands/create-user/create-user.command.ts diff --git a/libs/core/users/src/application/commands/create-user/create-user.test.ts b/libs/core/users/src/application/commands/create-user/create-user.test.ts new file mode 100644 index 0000000..2225b4b --- /dev/null +++ b/libs/core/users/src/application/commands/create-user/create-user.test.ts @@ -0,0 +1,93 @@ +import { CommandBus, CqrsModule } from '@nestjs/cqrs'; +import { UsersRepository, UsersRepositoryProvider } from '../../ports'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CreateUserCommandHandler } from './create-user.command-handler'; +import { CreateUserCommand } from './create-user.command'; +import { InMemoryUsersRepository } from '../../../infrastructure'; +import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; +import { UserMapper } from '../../mappers'; +import { UserEntity } from '../../../domain'; + +describe('Create user', () => { + let commandBus: CommandBus; + let repo: UsersRepository; + let eventEmitter: EventEmitter2; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [EventEmitterModule.forRoot(), CqrsModule.forRoot()], + providers: [ + UserMapper, + CreateUserCommandHandler, + { + provide: UsersRepositoryProvider, + useClass: InMemoryUsersRepository, + }, + ], + }).compile(); + + commandBus = module.get(CommandBus); + commandBus.register([CreateUserCommandHandler]); + eventEmitter = module.get(EventEmitter2); + repo = module.get(UsersRepositoryProvider); + }); + + it('should create a user', async () => { + const userToBeCreated = InMemoryUsersRepository.generateMockUser(); + const cmdResult = await commandBus.execute( + new CreateUserCommand({ + email: userToBeCreated.email, + username: userToBeCreated.username, + password: userToBeCreated.password, + fullname: userToBeCreated.fullname, + }) + ); + expect(cmdResult.isOk()).toBeTruthy(); + }, 100000); + + describe('Users already exist', () => { + it('should not create a user with pre-registered email', async () => { + const existingUserProps = + InMemoryUsersRepository.generateMockUser(); + const existingUser = UserEntity.create(existingUserProps); + await repo.insertOne(existingUser); + + const newUserWithSameEmail = { + email: existingUserProps.email, + username: 'newUsername', + password: 'newPassword', + fullname: 'New User', + }; + const cmdResult = await commandBus.execute( + new CreateUserCommand(newUserWithSameEmail) + ); + + expect(cmdResult.isErr()).toBeTruthy(); + expect(cmdResult.unwrapErr().message).toContain( + 'Email is already registered' + ); + }); + + it('should not create a user with pre-registered username', async () => { + const existingUserProps = + InMemoryUsersRepository.generateMockUser(); + const existingUser = UserEntity.create(existingUserProps); + await repo.insertOne(existingUser); + + const newUserWithSameUsername = { + email: 'newEmail@example.com', + username: existingUserProps.username, + password: 'newPassword', + fullname: 'New User', + }; + const cmdResult = await commandBus.execute( + new CreateUserCommand(newUserWithSameUsername) + ); + + expect(cmdResult.isErr()).toBeTruthy(); + expect(cmdResult.unwrapErr().message).toContain( + 'Username is already registered' + ); + }); + }); +}); diff --git a/libs/domain/users/src/application/commands/create-user/index.ts b/libs/core/users/src/application/commands/create-user/index.ts similarity index 100% rename from libs/domain/users/src/application/commands/create-user/index.ts rename to libs/core/users/src/application/commands/create-user/index.ts diff --git a/libs/domain/users/src/application/commands/delete-user/delete-user.command-handler.ts b/libs/core/users/src/application/commands/delete-user/delete-user.command-handler.ts similarity index 71% rename from libs/domain/users/src/application/commands/delete-user/delete-user.command-handler.ts rename to libs/core/users/src/application/commands/delete-user/delete-user.command-handler.ts index 38898d6..6181f53 100644 --- a/libs/domain/users/src/application/commands/delete-user/delete-user.command-handler.ts +++ b/libs/core/users/src/application/commands/delete-user/delete-user.command-handler.ts @@ -1,29 +1,26 @@ import { Inject, Logger } from '@nestjs/common'; import { DeleteUserCommand } from './delete-user.command'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { - WriteModelUsersRepository, -} from '../../ports/users.repository'; -import { - UserNotFoundError, -} from '../../../domain/errors'; +import { WriteModelUsersRepository } from '../../ports/users.repository'; +import { UserNotFoundError } from '../../../domain/errors'; import { Err, Ok, Result } from 'oxide.ts'; import { ConflictException, ExceptionBase } from '@goran/common'; @CommandHandler(DeleteUserCommand) export class DeleteUserCommandHandler - implements ICommandHandler { + implements ICommandHandler +{ private readonly logger = new Logger(DeleteUserCommandHandler.name); - constructor( - private readonly userRepo: WriteModelUsersRepository - ) { } + constructor(private readonly userRepo: WriteModelUsersRepository) {} async execute( command: DeleteUserCommand ): Promise> { this.logger.verbose( - `Processing user specifics with ${command.user.getProps().email} email` + `Processing user specifics with ${ + command.user.getProps().email + } email` ); this.logger.debug(`User specifics: ${JSON.stringify(command)}`); try { @@ -32,10 +29,11 @@ export class DeleteUserCommandHandler return Err(deletedUser.unwrapErr()); } this.logger.debug( - `User with ${command.user.getProps().email} email has been deleted` + `User with ${ + command.user.getProps().email + } email has been deleted` ); return Ok(deletedUser.unwrap()); - } catch (error: any) { this.logger.debug( `Unable to delete user with ${command.user.getProps().email}` diff --git a/libs/domain/users/src/application/commands/delete-user/delete-user.command.ts b/libs/core/users/src/application/commands/delete-user/delete-user.command.ts similarity index 50% rename from libs/domain/users/src/application/commands/delete-user/delete-user.command.ts rename to libs/core/users/src/application/commands/delete-user/delete-user.command.ts index a659c01..523b331 100644 --- a/libs/domain/users/src/application/commands/delete-user/delete-user.command.ts +++ b/libs/core/users/src/application/commands/delete-user/delete-user.command.ts @@ -2,10 +2,10 @@ import { Command, CommandProps } from '@goran/common'; import { UserEntity } from '@goran/users'; export class DeleteUserCommand extends Command { - readonly user: UserEntity; + readonly user: UserEntity; - constructor(props: CommandProps) { - super(props); - this.user = props.user; - } + constructor(props: CommandProps) { + super(props); + this.user = props.user; + } } diff --git a/libs/core/users/src/application/commands/delete-user/delete-user.test.ts b/libs/core/users/src/application/commands/delete-user/delete-user.test.ts new file mode 100644 index 0000000..2594ff5 --- /dev/null +++ b/libs/core/users/src/application/commands/delete-user/delete-user.test.ts @@ -0,0 +1,43 @@ +import { CommandBus, CqrsModule } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; +import { DeleteUserCommandHandler } from './delete-user.command-handler'; +import { + InMemoryUsersRepository, + UsersRepository, + UsersRepositoryProvider, + UserMapper, +} from '@goran/users'; + +describe('Create user', () => { + let commandBus: CommandBus; + let repo: UsersRepository; + let eventEmitter: EventEmitter2; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [EventEmitterModule.forRoot(), CqrsModule.forRoot()], + providers: [ + UserMapper, + DeleteUserCommandHandler, + { + provide: UsersRepositoryProvider, + useClass: InMemoryUsersRepository, + }, + ], + }).compile(); + + commandBus = module.get(CommandBus); + commandBus.register([DeleteUserCommandHandler]); + eventEmitter = module.get(EventEmitter2); + repo = module.get(UsersRepositoryProvider); + }); + + it('should delete a user successfully', async () => {}); + + it('should return an error if the user is not found', async () => {}); + + it('should handle a ConflictException and return a UserNotFoundError', async () => {}); + + it('should throw an error if an unexpected error occurs', async () => {}); +}); diff --git a/libs/domain/users/src/application/commands/delete-user/index.ts b/libs/core/users/src/application/commands/delete-user/index.ts similarity index 100% rename from libs/domain/users/src/application/commands/delete-user/index.ts rename to libs/core/users/src/application/commands/delete-user/index.ts diff --git a/libs/domain/users/src/application/commands/index.ts b/libs/core/users/src/application/commands/index.ts similarity index 100% rename from libs/domain/users/src/application/commands/index.ts rename to libs/core/users/src/application/commands/index.ts diff --git a/libs/core/users/src/application/decorators/current-user.decorator.ts b/libs/core/users/src/application/decorators/current-user.decorator.ts new file mode 100644 index 0000000..3109b00 --- /dev/null +++ b/libs/core/users/src/application/decorators/current-user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const CurrentUser = createParamDecorator( + (_, context: ExecutionContext) => { + const req = context.switchToHttp().getRequest(); + return req.user; + } +); diff --git a/libs/domain/users/src/application/decorators/index.ts b/libs/core/users/src/application/decorators/index.ts similarity index 50% rename from libs/domain/users/src/application/decorators/index.ts rename to libs/core/users/src/application/decorators/index.ts index 67e74a5..f7f76d8 100644 --- a/libs/domain/users/src/application/decorators/index.ts +++ b/libs/core/users/src/application/decorators/index.ts @@ -1 +1,2 @@ export * from './current-user.decorator'; +export * from './is-username.decorator'; diff --git a/libs/core/users/src/application/decorators/is-username.decorator.ts b/libs/core/users/src/application/decorators/is-username.decorator.ts new file mode 100644 index 0000000..def9127 --- /dev/null +++ b/libs/core/users/src/application/decorators/is-username.decorator.ts @@ -0,0 +1,23 @@ +import { registerDecorator, ValidationOptions } from 'class-validator'; + +/** + * Validates that a username correctness. + */ +export function IsUsername(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'IsUsername', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: string) { + return /^[a-z0-9_.-]{3,17}$/.test(value); + }, + defaultMessage() { + return "Only username that contain lowercase letters, numbers, '_', '-' and '.' with min 3 max 17 length"; + }, + }, + }); + }; +} diff --git a/libs/domain/users/src/application/index.ts b/libs/core/users/src/application/index.ts similarity index 100% rename from libs/domain/users/src/application/index.ts rename to libs/core/users/src/application/index.ts diff --git a/libs/domain/users/src/application/mappers/index.ts b/libs/core/users/src/application/mappers/index.ts similarity index 100% rename from libs/domain/users/src/application/mappers/index.ts rename to libs/core/users/src/application/mappers/index.ts diff --git a/libs/domain/users/src/application/mappers/user.mapper.ts b/libs/core/users/src/application/mappers/user.mapper.ts similarity index 100% rename from libs/domain/users/src/application/mappers/user.mapper.ts rename to libs/core/users/src/application/mappers/user.mapper.ts diff --git a/libs/domain/users/src/application/models/index.ts b/libs/core/users/src/application/models/index.ts similarity index 100% rename from libs/domain/users/src/application/models/index.ts rename to libs/core/users/src/application/models/index.ts diff --git a/libs/domain/users/src/application/models/user.model.ts b/libs/core/users/src/application/models/user.model.ts similarity index 100% rename from libs/domain/users/src/application/models/user.model.ts rename to libs/core/users/src/application/models/user.model.ts diff --git a/libs/domain/users/src/application/ports/index.ts b/libs/core/users/src/application/ports/index.ts similarity index 100% rename from libs/domain/users/src/application/ports/index.ts rename to libs/core/users/src/application/ports/index.ts diff --git a/libs/domain/users/src/application/ports/users.repository.ts b/libs/core/users/src/application/ports/users.repository.ts similarity index 100% rename from libs/domain/users/src/application/ports/users.repository.ts rename to libs/core/users/src/application/ports/users.repository.ts diff --git a/libs/domain/users/src/application/queries/findall-users-paginated/findall-users-paginated.query-handler.ts b/libs/core/users/src/application/queries/findall-users-paginated/findall-users-paginated.query-handler.ts similarity index 98% rename from libs/domain/users/src/application/queries/findall-users-paginated/findall-users-paginated.query-handler.ts rename to libs/core/users/src/application/queries/findall-users-paginated/findall-users-paginated.query-handler.ts index 469f992..2b2a902 100644 --- a/libs/domain/users/src/application/queries/findall-users-paginated/findall-users-paginated.query-handler.ts +++ b/libs/core/users/src/application/queries/findall-users-paginated/findall-users-paginated.query-handler.ts @@ -8,7 +8,7 @@ import { Ok, Result } from 'oxide.ts'; @QueryHandler(FindAllUsersPaginatedQuery) export class FindAllUsersPaginatedQueryHandler implements IQueryHandler { - constructor(private readonly userRepo: ReadModelUsersRepository) { } + constructor(private readonly userRepo: ReadModelUsersRepository) {} async execute( query: FindAllUsersPaginatedQuery diff --git a/libs/domain/users/src/application/queries/findall-users-paginated/findall-users-paginated.query.ts b/libs/core/users/src/application/queries/findall-users-paginated/findall-users-paginated.query.ts similarity index 58% rename from libs/domain/users/src/application/queries/findall-users-paginated/findall-users-paginated.query.ts rename to libs/core/users/src/application/queries/findall-users-paginated/findall-users-paginated.query.ts index be796eb..2fb21a7 100644 --- a/libs/domain/users/src/application/queries/findall-users-paginated/findall-users-paginated.query.ts +++ b/libs/core/users/src/application/queries/findall-users-paginated/findall-users-paginated.query.ts @@ -1,7 +1,7 @@ import { PaginatedParams, PaginatedQueryBase } from '@goran/common'; export class FindAllUsersPaginatedQuery extends PaginatedQueryBase { - constructor(props: PaginatedParams) { - super(props); - } + constructor(props: PaginatedParams) { + super(props); + } } diff --git a/libs/domain/users/src/application/queries/findall-users-paginated/index.ts b/libs/core/users/src/application/queries/findall-users-paginated/index.ts similarity index 100% rename from libs/domain/users/src/application/queries/findall-users-paginated/index.ts rename to libs/core/users/src/application/queries/findall-users-paginated/index.ts diff --git a/libs/domain/users/src/application/queries/findone-user-by-email/findone-user-by-email.query-handler.ts b/libs/core/users/src/application/queries/findone-user-by-email/findone-user-by-email.query-handler.ts similarity index 83% rename from libs/domain/users/src/application/queries/findone-user-by-email/findone-user-by-email.query-handler.ts rename to libs/core/users/src/application/queries/findone-user-by-email/findone-user-by-email.query-handler.ts index 4ee6c81..4634cfd 100644 --- a/libs/domain/users/src/application/queries/findone-user-by-email/findone-user-by-email.query-handler.ts +++ b/libs/core/users/src/application/queries/findone-user-by-email/findone-user-by-email.query-handler.ts @@ -8,7 +8,7 @@ import { ExceptionBase } from '@goran/common'; @QueryHandler(FindOneUserByEmailQuery) export class FindOneUserByEmailQueryHandler implements IQueryHandler { - constructor(private readonly userRepo: ReadModelUsersRepository) { } + constructor(private readonly userRepo: ReadModelUsersRepository) {} async execute( query: FindOneUserByEmailQuery @@ -17,9 +17,9 @@ export class FindOneUserByEmailQueryHandler implements IQueryHandler { return user.isSome() ? Ok(user.unwrap()) : Err( - new UserNotFoundError( - new Error('No such user found with corresponding email') - ) - ); + new UserNotFoundError( + new Error('No such user found with corresponding email') + ) + ); } } diff --git a/libs/core/users/src/application/queries/findone-user-by-email/findone-user-by-email.query.ts b/libs/core/users/src/application/queries/findone-user-by-email/findone-user-by-email.query.ts new file mode 100644 index 0000000..f0ba3f3 --- /dev/null +++ b/libs/core/users/src/application/queries/findone-user-by-email/findone-user-by-email.query.ts @@ -0,0 +1,10 @@ +import { QueryBase } from '@goran/common'; + +export class FindOneUserByEmailQuery extends QueryBase { + readonly email: string; + + constructor(email: string) { + super(); + this.email = email; + } +} diff --git a/libs/domain/users/src/application/queries/findone-user-by-email/index.ts b/libs/core/users/src/application/queries/findone-user-by-email/index.ts similarity index 100% rename from libs/domain/users/src/application/queries/findone-user-by-email/index.ts rename to libs/core/users/src/application/queries/findone-user-by-email/index.ts diff --git a/libs/domain/users/src/application/queries/findone-user-by-id/findone-user-by-id.query-handler.ts b/libs/core/users/src/application/queries/findone-user-by-id/findone-user-by-id.query-handler.ts similarity index 98% rename from libs/domain/users/src/application/queries/findone-user-by-id/findone-user-by-id.query-handler.ts rename to libs/core/users/src/application/queries/findone-user-by-id/findone-user-by-id.query-handler.ts index 556b670..1bc8dd4 100644 --- a/libs/domain/users/src/application/queries/findone-user-by-id/findone-user-by-id.query-handler.ts +++ b/libs/core/users/src/application/queries/findone-user-by-id/findone-user-by-id.query-handler.ts @@ -8,7 +8,7 @@ import { ExceptionBase } from '@goran/common'; @QueryHandler(FindOneUserByIdQuery) export class FindOneUserByIdQueryHandler implements IQueryHandler { - constructor(private readonly userRepo: ReadModelUsersRepository) { } + constructor(private readonly userRepo: ReadModelUsersRepository) {} async execute( query: FindOneUserByIdQuery diff --git a/libs/domain/users/src/application/queries/findone-user-by-id/findone-user-by-id.query.ts b/libs/core/users/src/application/queries/findone-user-by-id/findone-user-by-id.query.ts similarity index 50% rename from libs/domain/users/src/application/queries/findone-user-by-id/findone-user-by-id.query.ts rename to libs/core/users/src/application/queries/findone-user-by-id/findone-user-by-id.query.ts index 95e1aa8..3ebf13a 100644 --- a/libs/domain/users/src/application/queries/findone-user-by-id/findone-user-by-id.query.ts +++ b/libs/core/users/src/application/queries/findone-user-by-id/findone-user-by-id.query.ts @@ -1,10 +1,10 @@ import { QueryBase } from '@goran/common'; export class FindOneUserByIdQuery extends QueryBase { - readonly id: string; + readonly id: string; - constructor(id: string) { - super(); - this.id = id; - } + constructor(id: string) { + super(); + this.id = id; + } } diff --git a/libs/domain/users/src/application/queries/findone-user-by-id/index.ts b/libs/core/users/src/application/queries/findone-user-by-id/index.ts similarity index 100% rename from libs/domain/users/src/application/queries/findone-user-by-id/index.ts rename to libs/core/users/src/application/queries/findone-user-by-id/index.ts diff --git a/libs/domain/users/src/application/queries/findone-user-by-username/findone-user-by-username.query-handler.ts b/libs/core/users/src/application/queries/findone-user-by-username/findone-user-by-username.query-handler.ts similarity index 100% rename from libs/domain/users/src/application/queries/findone-user-by-username/findone-user-by-username.query-handler.ts rename to libs/core/users/src/application/queries/findone-user-by-username/findone-user-by-username.query-handler.ts diff --git a/libs/core/users/src/application/queries/findone-user-by-username/findone-user-by-username.query.ts b/libs/core/users/src/application/queries/findone-user-by-username/findone-user-by-username.query.ts new file mode 100644 index 0000000..6e35b35 --- /dev/null +++ b/libs/core/users/src/application/queries/findone-user-by-username/findone-user-by-username.query.ts @@ -0,0 +1,10 @@ +import { QueryBase } from '@goran/common'; + +export class FindOneUserByUsernameQuery extends QueryBase { + readonly username: string; + + constructor(username: string) { + super(); + this.username = username; + } +} diff --git a/libs/domain/users/src/application/queries/findone-user-by-username/index.ts b/libs/core/users/src/application/queries/findone-user-by-username/index.ts similarity index 100% rename from libs/domain/users/src/application/queries/findone-user-by-username/index.ts rename to libs/core/users/src/application/queries/findone-user-by-username/index.ts diff --git a/libs/domain/users/src/application/queries/index.ts b/libs/core/users/src/application/queries/index.ts similarity index 100% rename from libs/domain/users/src/application/queries/index.ts rename to libs/core/users/src/application/queries/index.ts diff --git a/libs/domain/users/src/application/services/index.ts b/libs/core/users/src/application/services/index.ts similarity index 100% rename from libs/domain/users/src/application/services/index.ts rename to libs/core/users/src/application/services/index.ts diff --git a/libs/domain/users/src/application/services/users.service.ts b/libs/core/users/src/application/services/users.service.ts similarity index 81% rename from libs/domain/users/src/application/services/users.service.ts rename to libs/core/users/src/application/services/users.service.ts index e7e6963..504cafa 100644 --- a/libs/domain/users/src/application/services/users.service.ts +++ b/libs/core/users/src/application/services/users.service.ts @@ -15,8 +15,8 @@ import { DeleteUserCommand, } from '../commands'; import { ProvideUsernameOrEmailError, UserEntity } from '../../domain'; -import { ExceptionBase } from '@goran/common'; -import { Err, Option, Result } from 'oxide.ts'; +import { ExceptionBase, Optional } from '@goran/common'; +import { Err, None, Option, Result, Some } from 'oxide.ts'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; @Injectable() @@ -25,7 +25,7 @@ export class UsersService { private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, private readonly mapper: UserMapper - ) { } + ) {} async changeEmail(userDto: UserModel, newEmail: string) { const user = await this.mapper.toDomain(userDto); @@ -80,8 +80,8 @@ export class UsersService { username, email, }: { - username?: string; - email?: string; + username: Optional; + email: Optional; }): Promise> { if (email) { return await this.findOneByEmail(email); @@ -96,6 +96,20 @@ export class UsersService { return await this.queryBus.execute(new FindOneUserByIdQuery(id)); } + /** + * @param id - User id + * @description Functions just like {findOneById} instead provide domain type instead of model. + * @returns User domain entity + */ + async findOneByIdDomain(id: string): Promise> { + const userModelOption = await this.queryBus.execute( + new FindOneUserByIdQuery(id) + ); + return userModelOption.isSome() + ? Some(await this.mapper.toDomain(userModelOption.unwrap())) + : None; + } + async delete(command: DeleteUserCommand) { return await this.commandBus.execute(command); } diff --git a/libs/domain/users/src/domain/entities/index.ts b/libs/core/users/src/domain/entities/index.ts similarity index 100% rename from libs/domain/users/src/domain/entities/index.ts rename to libs/core/users/src/domain/entities/index.ts diff --git a/libs/domain/users/src/domain/entities/user/index.ts b/libs/core/users/src/domain/entities/user/index.ts similarity index 100% rename from libs/domain/users/src/domain/entities/user/index.ts rename to libs/core/users/src/domain/entities/user/index.ts diff --git a/libs/domain/users/src/domain/entities/user/user.entity.ts b/libs/core/users/src/domain/entities/user/user.entity.ts similarity index 100% rename from libs/domain/users/src/domain/entities/user/user.entity.ts rename to libs/core/users/src/domain/entities/user/user.entity.ts diff --git a/libs/domain/users/src/domain/entities/user/user.types.ts b/libs/core/users/src/domain/entities/user/user.types.ts similarity index 100% rename from libs/domain/users/src/domain/entities/user/user.types.ts rename to libs/core/users/src/domain/entities/user/user.types.ts diff --git a/libs/core/users/src/domain/errors/email-already-registered.error.ts b/libs/core/users/src/domain/errors/email-already-registered.error.ts new file mode 100644 index 0000000..61b4d7f --- /dev/null +++ b/libs/core/users/src/domain/errors/email-already-registered.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class EmailAlreadyRegisteredError extends ExceptionBase { + static readonly message = 'Email is already registered'; + + public readonly code = 'USER.EMAIL.ALREADY_EXISTS'; + + constructor(cause?: Error, metadata?: unknown) { + super(EmailAlreadyRegisteredError.message, cause, metadata); + } +} diff --git a/libs/domain/users/src/domain/errors/index.ts b/libs/core/users/src/domain/errors/index.ts similarity index 100% rename from libs/domain/users/src/domain/errors/index.ts rename to libs/core/users/src/domain/errors/index.ts diff --git a/libs/core/users/src/domain/errors/invalid-password-reset-request.error.ts b/libs/core/users/src/domain/errors/invalid-password-reset-request.error.ts new file mode 100644 index 0000000..532ae15 --- /dev/null +++ b/libs/core/users/src/domain/errors/invalid-password-reset-request.error.ts @@ -0,0 +1,6 @@ +export class InvalidPasswordResetRequest extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidPasswordResetRequest'; + } +} diff --git a/libs/core/users/src/domain/errors/provide-username-or-email.error.ts b/libs/core/users/src/domain/errors/provide-username-or-email.error.ts new file mode 100644 index 0000000..afff6dc --- /dev/null +++ b/libs/core/users/src/domain/errors/provide-username-or-email.error.ts @@ -0,0 +1,10 @@ +import { ExceptionBase } from '@goran/common'; + +export class ProvideUsernameOrEmailError extends ExceptionBase { + static readonly message = 'Either username or email must be provided'; + public readonly code = 'AUTH.PROVIDE_USERNAME_OR_EMAIL'; + + constructor(cause?: Error, metadata?: unknown) { + super(ProvideUsernameOrEmailError.message, cause, metadata); + } +} diff --git a/libs/core/users/src/domain/errors/user-already-exists.error.ts b/libs/core/users/src/domain/errors/user-already-exists.error.ts new file mode 100644 index 0000000..0235fff --- /dev/null +++ b/libs/core/users/src/domain/errors/user-already-exists.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class UserAlreadyExistsError extends ExceptionBase { + static readonly message = 'User already exists'; + + public readonly code = 'USER.ALREADY_EXISTS'; + + constructor(cause?: Error, metadata?: unknown) { + super(UserAlreadyExistsError.message, cause, metadata); + } +} diff --git a/libs/core/users/src/domain/errors/user-creation-failed.error.ts b/libs/core/users/src/domain/errors/user-creation-failed.error.ts new file mode 100644 index 0000000..e790d0f --- /dev/null +++ b/libs/core/users/src/domain/errors/user-creation-failed.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class UserCreationFailedError extends ExceptionBase { + static readonly message = 'User creation failed'; + + public readonly code = 'USER.CREATION_FAILED'; + + constructor(cause?: Error, metadata?: unknown) { + super(UserCreationFailedError.message, cause, metadata); + } +} diff --git a/libs/domain/users/src/domain/errors/user-deletion-failed.error.ts b/libs/core/users/src/domain/errors/user-deletion-failed.error.ts similarity index 100% rename from libs/domain/users/src/domain/errors/user-deletion-failed.error.ts rename to libs/core/users/src/domain/errors/user-deletion-failed.error.ts diff --git a/libs/core/users/src/domain/errors/user-not-found.error.ts b/libs/core/users/src/domain/errors/user-not-found.error.ts new file mode 100644 index 0000000..1901a79 --- /dev/null +++ b/libs/core/users/src/domain/errors/user-not-found.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class UserNotFoundError extends ExceptionBase { + static readonly message = 'User not found'; + + public readonly code = 'USER.NOT_FOUND'; + + constructor(cause?: Error, metadata?: unknown) { + super(UserNotFoundError.message, cause, metadata); + } +} diff --git a/libs/core/users/src/domain/errors/username-already-registered.error.ts b/libs/core/users/src/domain/errors/username-already-registered.error.ts new file mode 100644 index 0000000..dfe8490 --- /dev/null +++ b/libs/core/users/src/domain/errors/username-already-registered.error.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@goran/common'; + +export class UsernameAlreadyRegisteredError extends ExceptionBase { + static readonly message = 'Username is already registered'; + + public readonly code = 'USER.USERNAME.ALREADY_EXISTS'; + + constructor(cause?: Error, metadata?: unknown) { + super(UsernameAlreadyRegisteredError.message, cause, metadata); + } +} diff --git a/libs/domain/users/src/domain/events/index.ts b/libs/core/users/src/domain/events/index.ts similarity index 100% rename from libs/domain/users/src/domain/events/index.ts rename to libs/core/users/src/domain/events/index.ts diff --git a/libs/domain/users/src/domain/events/user-created.domain-event.ts b/libs/core/users/src/domain/events/user-created.domain-event.ts similarity index 100% rename from libs/domain/users/src/domain/events/user-created.domain-event.ts rename to libs/core/users/src/domain/events/user-created.domain-event.ts diff --git a/libs/domain/users/src/domain/events/user-deleted.domain-event.ts b/libs/core/users/src/domain/events/user-deleted.domain-event.ts similarity index 56% rename from libs/domain/users/src/domain/events/user-deleted.domain-event.ts rename to libs/core/users/src/domain/events/user-deleted.domain-event.ts index 0790eac..455946b 100644 --- a/libs/domain/users/src/domain/events/user-deleted.domain-event.ts +++ b/libs/core/users/src/domain/events/user-deleted.domain-event.ts @@ -1,7 +1,7 @@ import { DomainEvent, DomainEventProps } from '@goran/common'; export class UserDeletedDomainEvent extends DomainEvent { - constructor(props: DomainEventProps) { - super(props); - } + constructor(props: DomainEventProps) { + super(props); + } } diff --git a/libs/domain/users/src/domain/events/user-email-updated.domain-event.ts b/libs/core/users/src/domain/events/user-email-updated.domain-event.ts similarity index 56% rename from libs/domain/users/src/domain/events/user-email-updated.domain-event.ts rename to libs/core/users/src/domain/events/user-email-updated.domain-event.ts index 0790eac..455946b 100644 --- a/libs/domain/users/src/domain/events/user-email-updated.domain-event.ts +++ b/libs/core/users/src/domain/events/user-email-updated.domain-event.ts @@ -1,7 +1,7 @@ import { DomainEvent, DomainEventProps } from '@goran/common'; export class UserDeletedDomainEvent extends DomainEvent { - constructor(props: DomainEventProps) { - super(props); - } + constructor(props: DomainEventProps) { + super(props); + } } diff --git a/libs/domain/users/src/domain/events/user-password-updated.domain-event.ts b/libs/core/users/src/domain/events/user-password-updated.domain-event.ts similarity index 56% rename from libs/domain/users/src/domain/events/user-password-updated.domain-event.ts rename to libs/core/users/src/domain/events/user-password-updated.domain-event.ts index 0790eac..455946b 100644 --- a/libs/domain/users/src/domain/events/user-password-updated.domain-event.ts +++ b/libs/core/users/src/domain/events/user-password-updated.domain-event.ts @@ -1,7 +1,7 @@ import { DomainEvent, DomainEventProps } from '@goran/common'; export class UserDeletedDomainEvent extends DomainEvent { - constructor(props: DomainEventProps) { - super(props); - } + constructor(props: DomainEventProps) { + super(props); + } } diff --git a/libs/domain/users/src/domain/events/user-username-updated.domain-event.ts b/libs/core/users/src/domain/events/user-username-updated.domain-event.ts similarity index 56% rename from libs/domain/users/src/domain/events/user-username-updated.domain-event.ts rename to libs/core/users/src/domain/events/user-username-updated.domain-event.ts index 0790eac..455946b 100644 --- a/libs/domain/users/src/domain/events/user-username-updated.domain-event.ts +++ b/libs/core/users/src/domain/events/user-username-updated.domain-event.ts @@ -1,7 +1,7 @@ import { DomainEvent, DomainEventProps } from '@goran/common'; export class UserDeletedDomainEvent extends DomainEvent { - constructor(props: DomainEventProps) { - super(props); - } + constructor(props: DomainEventProps) { + super(props); + } } diff --git a/libs/domain/users/src/domain/index.ts b/libs/core/users/src/domain/index.ts similarity index 100% rename from libs/domain/users/src/domain/index.ts rename to libs/core/users/src/domain/index.ts diff --git a/libs/core/users/src/domain/rules/index.ts b/libs/core/users/src/domain/rules/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/domain/users/src/domain/value-objects/index.ts b/libs/core/users/src/domain/value-objects/index.ts similarity index 100% rename from libs/domain/users/src/domain/value-objects/index.ts rename to libs/core/users/src/domain/value-objects/index.ts diff --git a/libs/domain/users/src/domain/value-objects/password.ts b/libs/core/users/src/domain/value-objects/password.ts similarity index 81% rename from libs/domain/users/src/domain/value-objects/password.ts rename to libs/core/users/src/domain/value-objects/password.ts index 6470fbe..409efec 100644 --- a/libs/domain/users/src/domain/value-objects/password.ts +++ b/libs/core/users/src/domain/value-objects/password.ts @@ -10,7 +10,9 @@ export class PasswordValueObject extends ValueObject { Guard.isEmpty(props.value) || props.value.length > 35 || props.value.length < 8 || - !props.value.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]+$/) + !props.value.match( + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]+$/ + ) ) { throw new Error('Invalid password format.'); } diff --git a/libs/domain/users/src/index.ts b/libs/core/users/src/index.ts similarity index 100% rename from libs/domain/users/src/index.ts rename to libs/core/users/src/index.ts diff --git a/libs/core/users/src/infrastructure/index.ts b/libs/core/users/src/infrastructure/index.ts new file mode 100644 index 0000000..ca18ac1 --- /dev/null +++ b/libs/core/users/src/infrastructure/index.ts @@ -0,0 +1 @@ +export * from './persistence'; diff --git a/libs/domain/users/src/infrastructure/persistence/drizzle/index.ts b/libs/core/users/src/infrastructure/persistence/drizzle/index.ts similarity index 100% rename from libs/domain/users/src/infrastructure/persistence/drizzle/index.ts rename to libs/core/users/src/infrastructure/persistence/drizzle/index.ts diff --git a/libs/domain/users/src/infrastructure/persistence/drizzle/postgres-drizzle-users.repository.ts b/libs/core/users/src/infrastructure/persistence/drizzle/postgres-drizzle-users.repository.ts similarity index 95% rename from libs/domain/users/src/infrastructure/persistence/drizzle/postgres-drizzle-users.repository.ts rename to libs/core/users/src/infrastructure/persistence/drizzle/postgres-drizzle-users.repository.ts index 1c2bed2..5c6f3e1 100644 --- a/libs/domain/users/src/infrastructure/persistence/drizzle/postgres-drizzle-users.repository.ts +++ b/libs/core/users/src/infrastructure/persistence/drizzle/postgres-drizzle-users.repository.ts @@ -1,7 +1,12 @@ import { Injectable } from '@nestjs/common'; import { eq } from 'drizzle-orm/expressions'; import { DrizzleService, UsersDataPgTable } from '@goran/drizzle-data-access'; -import { UserMapper, UserModel, WriteModelUsersRepository, ReadModelUsersRepository } from '../../../application'; +import { + UserMapper, + UserModel, + WriteModelUsersRepository, + ReadModelUsersRepository, +} from '../../../application'; import { UserAlreadyExistsError, UserNotFoundError, @@ -14,11 +19,14 @@ import { ExceptionBase, Paginated, PaginatedQueryParams } from '@goran/common'; @Injectable() export class PostgreSqlDrizzleUsersRepository - implements Partial, Partial { + implements + Partial, + Partial +{ constructor( private readonly drizzleService: DrizzleService, private readonly mapper: UserMapper - ) { } + ) {} async findAllPaginated( params: PaginatedQueryParams diff --git a/libs/domain/users/src/infrastructure/persistence/in-memory/in-memory-users.repository.ts b/libs/core/users/src/infrastructure/persistence/in-memory/in-memory-users.repository.ts similarity index 95% rename from libs/domain/users/src/infrastructure/persistence/in-memory/in-memory-users.repository.ts rename to libs/core/users/src/infrastructure/persistence/in-memory/in-memory-users.repository.ts index 4a76024..72396cb 100644 --- a/libs/domain/users/src/infrastructure/persistence/in-memory/in-memory-users.repository.ts +++ b/libs/core/users/src/infrastructure/persistence/in-memory/in-memory-users.repository.ts @@ -1,5 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { UserMapper, UserModel, WriteModelUsersRepository, ReadModelUsersRepository } from '../../../application'; +import { + UserMapper, + UserModel, + WriteModelUsersRepository, + ReadModelUsersRepository, +} from '../../../application'; import { EmailAlreadyRegisteredError, UserEntity, @@ -14,8 +19,10 @@ import { Err, None, Ok, Option, Result, Some } from 'oxide.ts'; @Injectable() export class InMemoryUsersRepository - implements Partial, Partial { - + implements + Partial, + Partial +{ constructor(private readonly mapper: UserMapper) { this.users = []; } diff --git a/libs/domain/users/src/infrastructure/persistence/in-memory/index.ts b/libs/core/users/src/infrastructure/persistence/in-memory/index.ts similarity index 100% rename from libs/domain/users/src/infrastructure/persistence/in-memory/index.ts rename to libs/core/users/src/infrastructure/persistence/in-memory/index.ts diff --git a/libs/domain/users/src/infrastructure/persistence/index.ts b/libs/core/users/src/infrastructure/persistence/index.ts similarity index 100% rename from libs/domain/users/src/infrastructure/persistence/index.ts rename to libs/core/users/src/infrastructure/persistence/index.ts diff --git a/libs/domain/users/src/users.module.ts b/libs/core/users/src/users.module.ts similarity index 92% rename from libs/domain/users/src/users.module.ts rename to libs/core/users/src/users.module.ts index 01bdc9c..870307a 100644 --- a/libs/domain/users/src/users.module.ts +++ b/libs/core/users/src/users.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { UsersService } from './application/services'; import { CreateUserCommandHandler } from './application/commands/create-user'; import { DeleteUserCommandHandler } from './application/commands/delete-user/delete-user.command-handler'; @@ -14,7 +14,6 @@ import { } from './application/ports'; import { PostgreSqlDrizzleUsersRepository } from './infrastructure/persistence/drizzle/postgres-drizzle-users.repository'; import { UserMapper } from './application/mappers/user.mapper'; -import { CqrsModule } from '@nestjs/cqrs'; const writeRepo = { provide: WriteModelUsersRepository, @@ -35,8 +34,8 @@ const queries = [ FindOneUserByUsernameQueryHandler, ]; +@Global() @Module({ - imports: [CqrsModule], providers: [ UserMapper, ...queries, diff --git a/libs/core/users/tsconfig.json b/libs/core/users/tsconfig.json new file mode 100644 index 0000000..03d08bc --- /dev/null +++ b/libs/core/users/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/core/users/tsconfig.lib.json b/libs/core/users/tsconfig.lib.json new file mode 100644 index 0000000..f8aeacc --- /dev/null +++ b/libs/core/users/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/core/users/tsconfig.spec.json b/libs/core/users/tsconfig.spec.json new file mode 100644 index 0000000..adff492 --- /dev/null +++ b/libs/core/users/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/data-access/drizzle/src/drizzle/0002_users-auth-sessions.sql b/libs/data-access/drizzle/src/drizzle/0002_users-auth-sessions.sql new file mode 100644 index 0000000..0c148ee --- /dev/null +++ b/libs/data-access/drizzle/src/drizzle/0002_users-auth-sessions.sql @@ -0,0 +1,28 @@ +DO $$ BEGIN + CREATE TYPE "public"."session_status" AS ENUM('active', 'revoked'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "status" "session_status" DEFAULT 'active' NOT NULL, + "refresh_token" text NOT NULL, + "location" text, + "device" text, + "ip" text NOT NULL, + "expire" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "sessions_refresh_token_unique" UNIQUE("refresh_token") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "unique_refresh_token" ON "sessions" USING btree ("refresh_token");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "unique_user_idx" ON "sessions" USING btree ("user_id"); \ No newline at end of file diff --git a/libs/data-access/drizzle/src/drizzle/meta/0002_snapshot.json b/libs/data-access/drizzle/src/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..5ba02ac --- /dev/null +++ b/libs/data-access/drizzle/src/drizzle/meta/0002_snapshot.json @@ -0,0 +1,325 @@ +{ + "id": "d3d7d16a-bfe6-4ec5-9db1-43922fdddad3", + "prevId": "1890d89d-bd1b-405a-8d11-3fc8b9572365", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.password_reset_requests": { + "name": "password_reset_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "password_reset_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "otpcode": { + "name": "otpcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_requests_user_id_users_id_fk": { + "name": "password_reset_requests_user_id_users_id_fk", + "tableFrom": "password_reset_requests", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_requests_token_unique": { + "name": "password_reset_requests_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expire": { + "name": "expire", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_refresh_token": { + "name": "unique_refresh_token", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_user_idx": { + "name": "unique_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_refresh_token_unique": { + "name": "sessions_refresh_token_unique", + "nullsNotDistinct": false, + "columns": [ + "refresh_token" + ] + } + } + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_idx": { + "name": "unique_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + } + }, + "enums": { + "public.password_reset_status": { + "name": "password_reset_status", + "schema": "public", + "values": [ + "requested", + "verified", + "successful", + "dismissed" + ] + }, + "public.session_status": { + "name": "session_status", + "schema": "public", + "values": [ + "active", + "revoked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/libs/data-access/drizzle/src/drizzle/meta/_journal.json b/libs/data-access/drizzle/src/drizzle/meta/_journal.json index 563285b..c7a1807 100644 --- a/libs/data-access/drizzle/src/drizzle/meta/_journal.json +++ b/libs/data-access/drizzle/src/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1724437474829, "tag": "0001_add-password-reset", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1726321256960, + "tag": "0002_users-auth-sessions", + "breakpoints": true } ] } \ No newline at end of file diff --git a/libs/data-access/drizzle/src/schema.ts b/libs/data-access/drizzle/src/schema.ts index 97bcd1c..28a8c85 100644 --- a/libs/data-access/drizzle/src/schema.ts +++ b/libs/data-access/drizzle/src/schema.ts @@ -4,13 +4,16 @@ import { timestamp, uniqueIndex, pgEnum, + date, } from 'drizzle-orm/pg-core'; -import { ulid } from "ulid"; +import { ulid } from 'ulid'; export const UsersDataPgTable = pgTable( 'users', { - id: text('id').primaryKey().$defaultFn(() => ulid()), + id: text('id') + .primaryKey() + .$defaultFn(() => ulid()), fullname: text('fullname'), username: text('name').notNull().unique(), password: text('password').notNull(), @@ -25,25 +28,71 @@ export const UsersDataPgTable = pgTable( } ); -export const passwordResetStatusEnum = pgEnum('password_reset_status', ['requested', 'verified', 'successful', 'dismissed']) +export const passwordResetStatusEnum = pgEnum('password_reset_status', [ + 'requested', + 'verified', + 'successful', + 'dismissed', +]); export const PasswordResetRequestsDataPgTable = pgTable( 'password_reset_requests', { - id: text('id').primaryKey().$defaultFn(() => ulid()), - userId: text('user_id').references(() => UsersDataPgTable.id).notNull(), - status: passwordResetStatusEnum('status').notNull().default('requested'), + id: text('id') + .primaryKey() + .$defaultFn(() => ulid()), + userId: text('user_id') + .references(() => UsersDataPgTable.id) + .notNull(), + status: passwordResetStatusEnum('status') + .notNull() + .default('requested'), token: text('token').notNull().unique(), otpcode: text('otpcode').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), + } +); + +export const sessionStatusEnum = pgEnum('session_status', [ + 'active', + 'revoked', +]); + +export const SessionsDataPgTable = pgTable( + 'sessions', + { + id: text('id') + .primaryKey() + .$defaultFn(() => ulid()), + userId: text('user_id') + .references(() => UsersDataPgTable.id) + .notNull(), + status: sessionStatusEnum('status').notNull().default('active'), + refreshToken: text('refresh_token').notNull().unique(), + location: text('location'), + device: text('device'), + ip: text('ip').notNull(), + expire: timestamp('expire').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), }, + (sessions) => { + return { + uniquerefreshToken: uniqueIndex('unique_refresh_token').on( + sessions.refreshToken + ), + uniqueUserIdx: uniqueIndex('unique_user_idx').on(sessions.userId), + }; + } ); const schema = { UsersDataPgTable, PasswordResetRequestsDataPgTable, passwordResetStatusEnum, + sessionStatusEnum, + SessionsDataPgTable, }; export default schema; diff --git a/libs/data-access/drizzle/tsconfig.json b/libs/data-access/drizzle/tsconfig.json index feb34a0..d28adcf 100644 --- a/libs/data-access/drizzle/tsconfig.json +++ b/libs/data-access/drizzle/tsconfig.json @@ -1,25 +1,26 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "ts-node": { + "extends": "../../../tsconfig.base.json", "compilerOptions": { - "module": "commonjs", - "target": "ES2023" - } - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "strictNullChecks": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "ts-node": { + "compilerOptions": { + "module": "commonjs", + "target": "ES2023" + } + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] } diff --git a/libs/domain/users/.eslintrc.json b/libs/domain/users/.eslintrc.json deleted file mode 100644 index 3456be9..0000000 --- a/libs/domain/users/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/domain/users/project.json b/libs/domain/users/project.json deleted file mode 100644 index 42407d0..0000000 --- a/libs/domain/users/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "users", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/domain/users/src", - "projectType": "library", - "tags": [], - "// targets": "to see all targets run: nx show project users --web", - "targets": { - "test": { - "executor": "@nx/jest:jest", - "options": { - "jestConfig": "libs/domain/users/jest.config.ts", - "passWithNoTests": true - } - } - } -} diff --git a/libs/domain/users/src/application/commands/change-user-email/change-user-email.command.ts b/libs/domain/users/src/application/commands/change-user-email/change-user-email.command.ts deleted file mode 100644 index 38ea923..0000000 --- a/libs/domain/users/src/application/commands/change-user-email/change-user-email.command.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Command, CommandProps } from '@goran/common'; -import { UserEntity } from '@goran/users'; - -export class ChangeUserEmailCommand extends Command { - readonly newEmail: string; - readonly user: UserEntity; - - constructor(props: CommandProps) { - super(props); - this.newEmail = props.newEmail; - this.user = props.user; - } -} diff --git a/libs/domain/users/src/application/commands/change-user-email/change-user-email.test.ts b/libs/domain/users/src/application/commands/change-user-email/change-user-email.test.ts deleted file mode 100644 index 8b13789..0000000 --- a/libs/domain/users/src/application/commands/change-user-email/change-user-email.test.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libs/domain/users/src/application/commands/change-user-password/change-user-password.test.ts b/libs/domain/users/src/application/commands/change-user-password/change-user-password.test.ts deleted file mode 100644 index 8b13789..0000000 --- a/libs/domain/users/src/application/commands/change-user-password/change-user-password.test.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libs/domain/users/src/application/commands/change-user-username/change-user-username.test.ts b/libs/domain/users/src/application/commands/change-user-username/change-user-username.test.ts deleted file mode 100644 index 8b13789..0000000 --- a/libs/domain/users/src/application/commands/change-user-username/change-user-username.test.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libs/domain/users/src/application/commands/create-user/create-user.test.ts b/libs/domain/users/src/application/commands/create-user/create-user.test.ts deleted file mode 100644 index ac3db36..0000000 --- a/libs/domain/users/src/application/commands/create-user/create-user.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { CommandBus, CqrsModule } from '@nestjs/cqrs'; -import { UsersRepository, UsersRepositoryProvider } from '../../ports'; -import { Test, TestingModule } from '@nestjs/testing'; -import { CreateUserCommandHandler } from './create-user.command-handler'; -import { CreateUserCommand } from './create-user.command'; -import { InMemoryUsersRepository } from '../../../infrastructure'; -import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; -import { UserMapper } from '../../mappers'; -import { UserEntity } from '../../../domain'; - -describe('Create user', () => { - let commandBus: CommandBus; - let repo: UsersRepository; - let eventEmitter: EventEmitter2; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [EventEmitterModule.forRoot(), CqrsModule.forRoot()], - providers: [ - UserMapper, - CreateUserCommandHandler, - { - provide: UsersRepositoryProvider, - useClass: InMemoryUsersRepository, - }, - ], - }).compile(); - - commandBus = module.get(CommandBus); - commandBus.register([CreateUserCommandHandler]); - eventEmitter = module.get(EventEmitter2); - repo = module.get(UsersRepositoryProvider); - }); - - it('should create a user', async () => { - const userToBeCreated = InMemoryUsersRepository.generateMockUser(); - const cmdResult = await commandBus.execute( - new CreateUserCommand({ - email: userToBeCreated.email, - username: userToBeCreated.username, - password: userToBeCreated.password, - fullname: userToBeCreated.fullname, - }) - ); - expect(cmdResult.isOk()).toBeTruthy(); - }, 100000); - - describe('Users already exist', () => { - it('should not create a user with pre-registered email', async () => { - const existingUserProps = InMemoryUsersRepository.generateMockUser(); - const existingUser = UserEntity.create(existingUserProps); - await repo.insertOne(existingUser); - - const newUserWithSameEmail = { - email: existingUserProps.email, - username: 'newUsername', - password: 'newPassword', - fullname: 'New User', - }; - const cmdResult = await commandBus.execute( - new CreateUserCommand(newUserWithSameEmail) - ); - - expect(cmdResult.isErr()).toBeTruthy(); - expect(cmdResult.unwrapErr().message).toContain( - 'Email is already registered' - ); - }); - - it('should not create a user with pre-registered username', async () => { - const existingUserProps = InMemoryUsersRepository.generateMockUser(); - const existingUser = UserEntity.create(existingUserProps); - await repo.insertOne(existingUser); - - const newUserWithSameUsername = { - email: 'newEmail@example.com', - username: existingUserProps.username, - password: 'newPassword', - fullname: 'New User', - }; - const cmdResult = await commandBus.execute( - new CreateUserCommand(newUserWithSameUsername) - ); - - expect(cmdResult.isErr()).toBeTruthy(); - expect(cmdResult.unwrapErr().message).toContain( - 'Username is already registered' - ); - }); - }); -}); diff --git a/libs/domain/users/src/application/commands/delete-user/delete-user.test.ts b/libs/domain/users/src/application/commands/delete-user/delete-user.test.ts deleted file mode 100644 index 45a4511..0000000 --- a/libs/domain/users/src/application/commands/delete-user/delete-user.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CommandBus, CqrsModule } from '@nestjs/cqrs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; -import { DeleteUserCommandHandler } from './delete-user.command-handler'; -import { - InMemoryUsersRepository, - UsersRepository, - UsersRepositoryProvider, - UserMapper, -} from '@goran/users'; - -describe('Create user', () => { - let commandBus: CommandBus; - let repo: UsersRepository; - let eventEmitter: EventEmitter2; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [EventEmitterModule.forRoot(), CqrsModule.forRoot()], - providers: [ - UserMapper, - DeleteUserCommandHandler, - { - provide: UsersRepositoryProvider, - useClass: InMemoryUsersRepository, - }, - ], - }).compile(); - - commandBus = module.get(CommandBus); - commandBus.register([DeleteUserCommandHandler]); - eventEmitter = module.get(EventEmitter2); - repo = module.get(UsersRepositoryProvider); - }); - - it('should delete a user successfully', async () => {}); - - it('should return an error if the user is not found', async () => {}); - - it('should handle a ConflictException and return a UserNotFoundError', async () => {}); - - it('should throw an error if an unexpected error occurs', async () => {}); -}); diff --git a/libs/domain/users/src/application/decorators/current-user.decorator.ts b/libs/domain/users/src/application/decorators/current-user.decorator.ts deleted file mode 100644 index 01f2033..0000000 --- a/libs/domain/users/src/application/decorators/current-user.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; - -export const CurrentUser = createParamDecorator( - (_, context: ExecutionContext) => { - const req = context.switchToHttp().getRequest(); - return req.user; - } -); diff --git a/libs/domain/users/src/application/queries/findone-user-by-email/findone-user-by-email.query.ts b/libs/domain/users/src/application/queries/findone-user-by-email/findone-user-by-email.query.ts deleted file mode 100644 index a08e467..0000000 --- a/libs/domain/users/src/application/queries/findone-user-by-email/findone-user-by-email.query.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { QueryBase } from '@goran/common'; - -export class FindOneUserByEmailQuery extends QueryBase { - readonly email: string; - - constructor(email: string) { - super(); - this.email = email; - } -} diff --git a/libs/domain/users/src/application/queries/findone-user-by-username/findone-user-by-username.query.ts b/libs/domain/users/src/application/queries/findone-user-by-username/findone-user-by-username.query.ts deleted file mode 100644 index 33b7ac1..0000000 --- a/libs/domain/users/src/application/queries/findone-user-by-username/findone-user-by-username.query.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { QueryBase } from '@goran/common'; - -export class FindOneUserByUsernameQuery extends QueryBase { - readonly username: string; - - constructor(username: string) { - super(); - this.username = username; - } -} diff --git a/libs/domain/users/src/domain/errors/email-already-registered.error.ts b/libs/domain/users/src/domain/errors/email-already-registered.error.ts deleted file mode 100644 index c133cd5..0000000 --- a/libs/domain/users/src/domain/errors/email-already-registered.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExceptionBase } from '@goran/common'; - -export class EmailAlreadyRegisteredError extends ExceptionBase { - static readonly message = 'Email is already registered'; - - public readonly code = 'USER.EMAIL.ALREADY_EXISTS'; - - constructor(cause?: Error, metadata?: unknown) { - super(EmailAlreadyRegisteredError.message, cause, metadata); - } -} diff --git a/libs/domain/users/src/domain/errors/invalid-password-reset-request.error.ts b/libs/domain/users/src/domain/errors/invalid-password-reset-request.error.ts deleted file mode 100644 index 7e456d8..0000000 --- a/libs/domain/users/src/domain/errors/invalid-password-reset-request.error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class InvalidPasswordResetRequest extends Error { - constructor(message: string) { - super(message); - this.name = 'InvalidPasswordResetRequest'; - } -} diff --git a/libs/domain/users/src/domain/errors/provide-username-or-email.error.ts b/libs/domain/users/src/domain/errors/provide-username-or-email.error.ts deleted file mode 100644 index efae572..0000000 --- a/libs/domain/users/src/domain/errors/provide-username-or-email.error.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ExceptionBase } from '@goran/common'; - -export class ProvideUsernameOrEmailError extends ExceptionBase { - static readonly message = 'Either username or email must be provided'; - public readonly code = 'AUTH.PROVIDE_USERNAME_OR_EMAIL'; - - constructor(cause?: Error, metadata?: unknown) { - super(ProvideUsernameOrEmailError.message, cause, metadata); - } -} diff --git a/libs/domain/users/src/domain/errors/user-already-exists.error.ts b/libs/domain/users/src/domain/errors/user-already-exists.error.ts deleted file mode 100644 index 79ac665..0000000 --- a/libs/domain/users/src/domain/errors/user-already-exists.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExceptionBase } from '@goran/common'; - -export class UserAlreadyExistsError extends ExceptionBase { - static readonly message = 'User already exists'; - - public readonly code = 'USER.ALREADY_EXISTS'; - - constructor(cause?: Error, metadata?: unknown) { - super(UserAlreadyExistsError.message, cause, metadata); - } -} diff --git a/libs/domain/users/src/domain/errors/user-creation-failed.error.ts b/libs/domain/users/src/domain/errors/user-creation-failed.error.ts deleted file mode 100644 index 8dab547..0000000 --- a/libs/domain/users/src/domain/errors/user-creation-failed.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExceptionBase } from '@goran/common'; - -export class UserCreationFailedError extends ExceptionBase { - static readonly message = 'User creation failed'; - - public readonly code = 'USER.CREATION_FAILED'; - - constructor(cause?: Error, metadata?: unknown) { - super(UserCreationFailedError.message, cause, metadata); - } -} diff --git a/libs/domain/users/src/domain/errors/user-not-found.error.ts b/libs/domain/users/src/domain/errors/user-not-found.error.ts deleted file mode 100644 index 4192af2..0000000 --- a/libs/domain/users/src/domain/errors/user-not-found.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExceptionBase } from '@goran/common'; - -export class UserNotFoundError extends ExceptionBase { - static readonly message = 'User not found'; - - public readonly code = 'USER.NOT_FOUND'; - - constructor(cause?: Error, metadata?: unknown) { - super(UserNotFoundError.message, cause, metadata); - } -} diff --git a/libs/domain/users/src/domain/errors/username-already-registered.error.ts b/libs/domain/users/src/domain/errors/username-already-registered.error.ts deleted file mode 100644 index e210fe5..0000000 --- a/libs/domain/users/src/domain/errors/username-already-registered.error.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ExceptionBase } from '@goran/common'; - -export class UsernameAlreadyRegisteredError extends ExceptionBase { - static readonly message = 'Username is already registered'; - - public readonly code = 'USER.USERNAME.ALREADY_EXISTS'; - - constructor(cause?: Error, metadata?: unknown) { - super(UsernameAlreadyRegisteredError.message, cause, metadata); - } -} diff --git a/libs/domain/users/tsconfig.json b/libs/domain/users/tsconfig.json deleted file mode 100644 index 8122543..0000000 --- a/libs/domain/users/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/domain/users/tsconfig.lib.json b/libs/domain/users/tsconfig.lib.json deleted file mode 100644 index dbf54fd..0000000 --- a/libs/domain/users/tsconfig.lib.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"], - "target": "es2021", - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/domain/users/tsconfig.spec.json b/libs/domain/users/tsconfig.spec.json deleted file mode 100644 index 69a251f..0000000 --- a/libs/domain/users/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/libs/generics/device-detector/.eslintrc.json b/libs/generics/device-detector/.eslintrc.json new file mode 100644 index 0000000..9761c56 --- /dev/null +++ b/libs/generics/device-detector/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/generics/device-detector/README.md b/libs/generics/device-detector/README.md new file mode 100644 index 0000000..ad233e3 --- /dev/null +++ b/libs/generics/device-detector/README.md @@ -0,0 +1,7 @@ +# device-detector + +Thanks to [node-device-detector](https://github.com/sanchezzzhak/node-device-detector) + +## Running unit tests + +Run `nx test device-detector` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/generics/device-detector/jest.config.ts b/libs/generics/device-detector/jest.config.ts new file mode 100644 index 0000000..357723f --- /dev/null +++ b/libs/generics/device-detector/jest.config.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +export default { + displayName: 'device-detector', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': [ + 'ts-jest', + { tsconfig: '/tsconfig.spec.json' }, + ], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/generics/device-detector', +}; diff --git a/libs/generics/device-detector/project.json b/libs/generics/device-detector/project.json new file mode 100644 index 0000000..b0f3b75 --- /dev/null +++ b/libs/generics/device-detector/project.json @@ -0,0 +1,9 @@ +{ + "name": "device-detector", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/generics/device-detector/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project device-detector --web", + "targets": {} +} diff --git a/libs/generics/device-detector/src/index.ts b/libs/generics/device-detector/src/index.ts new file mode 100644 index 0000000..c92d1c5 --- /dev/null +++ b/libs/generics/device-detector/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/device-detector.module'; +export * from './lib/device-detector.service'; diff --git a/libs/generics/device-detector/src/lib/device-detector.module.ts b/libs/generics/device-detector/src/lib/device-detector.module.ts new file mode 100644 index 0000000..64e2668 --- /dev/null +++ b/libs/generics/device-detector/src/lib/device-detector.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { DeviceDetectorService } from './device-detector.service'; + +@Global() +@Module({ + providers: [DeviceDetectorService], + exports: [DeviceDetectorService], +}) +export class DeviceDetectorModule { } diff --git a/libs/generics/device-detector/src/lib/device-detector.service.ts b/libs/generics/device-detector/src/lib/device-detector.service.ts new file mode 100644 index 0000000..ed360d4 --- /dev/null +++ b/libs/generics/device-detector/src/lib/device-detector.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import DeviceDetector from 'node-device-detector'; + +@Injectable() +export class DeviceDetectorService { + private readonly detector = new DeviceDetector({ + clientIndexes: true, + deviceIndexes: true, + deviceAliasCode: false, + deviceTrusted: false, + deviceInfo: false, + maxUserAgentSize: 500, + }); + constructor() { } + + /** + * detect + */ + public detect(userAgent: string) { + return this.detector.detect(userAgent); + } + + /** + * @param userAgent - The client user agent + * @returns Formatted device name with os and browser name + * @example Android (5.0) / Nubia Z7 Max (Chrome Mobile) + */ + public getDevice(userAgent: string) { + const result = this.detector.detect(userAgent); + return `${result.os.name} (${result.os.version}) / ${result.device.model} (${result.client.name})`; + } +} diff --git a/libs/generics/device-detector/tsconfig.json b/libs/generics/device-detector/tsconfig.json new file mode 100644 index 0000000..03d08bc --- /dev/null +++ b/libs/generics/device-detector/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/generics/device-detector/tsconfig.lib.json b/libs/generics/device-detector/tsconfig.lib.json new file mode 100644 index 0000000..f8aeacc --- /dev/null +++ b/libs/generics/device-detector/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/generics/device-detector/tsconfig.spec.json b/libs/generics/device-detector/tsconfig.spec.json new file mode 100644 index 0000000..adff492 --- /dev/null +++ b/libs/generics/device-detector/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/generics/ip-locator/.eslintrc.json b/libs/generics/ip-locator/.eslintrc.json new file mode 100644 index 0000000..9761c56 --- /dev/null +++ b/libs/generics/ip-locator/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/generics/ip-locator/README.md b/libs/generics/ip-locator/README.md new file mode 100644 index 0000000..5c85f64 --- /dev/null +++ b/libs/generics/ip-locator/README.md @@ -0,0 +1,14 @@ +# @goran/ip-locator + +Providing IP locationing functionality + +## Usage + +1. Import the `IpLocatorModule` in your module +2. Inject `IpLocatorService` to your provider + +## Running unit tests + +Run `nx test ip-locator` to execute the unit tests via [Jest](https://jestjs.io). + +Thanks to [ip-api](https://ip-api.com/) diff --git a/libs/generics/ip-locator/jest.config.ts b/libs/generics/ip-locator/jest.config.ts new file mode 100644 index 0000000..1c65d7a --- /dev/null +++ b/libs/generics/ip-locator/jest.config.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +export default { + displayName: 'ip-locator', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': [ + 'ts-jest', + { tsconfig: '/tsconfig.spec.json' }, + ], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/generics/ip-locator', +}; diff --git a/libs/generics/ip-locator/project.json b/libs/generics/ip-locator/project.json new file mode 100644 index 0000000..51f58c4 --- /dev/null +++ b/libs/generics/ip-locator/project.json @@ -0,0 +1,9 @@ +{ + "name": "ip-locator", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/generics/ip-locator/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ip-locator --web", + "targets": {} +} diff --git a/libs/generics/ip-locator/src/index.ts b/libs/generics/ip-locator/src/index.ts new file mode 100644 index 0000000..b420e58 --- /dev/null +++ b/libs/generics/ip-locator/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/ip-locator.module'; +export * from './lib/ip-locator.service'; +export * from './lib/types'; diff --git a/libs/generics/ip-locator/src/lib/ip-locator.module.ts b/libs/generics/ip-locator/src/lib/ip-locator.module.ts new file mode 100644 index 0000000..2eaec21 --- /dev/null +++ b/libs/generics/ip-locator/src/lib/ip-locator.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { IpLocatorService } from './ip-locator.service'; +import { HttpModule } from '@nestjs/axios'; + +@Global() +@Module({ + providers: [IpLocatorService], + imports: [HttpModule], + exports: [IpLocatorService], +}) +export class IpLocatorModule {} diff --git a/libs/generics/ip-locator/src/lib/ip-locator.service.ts b/libs/generics/ip-locator/src/lib/ip-locator.service.ts new file mode 100644 index 0000000..1ddd5de --- /dev/null +++ b/libs/generics/ip-locator/src/lib/ip-locator.service.ts @@ -0,0 +1,61 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; +import { Geolocation, IpLocationDto } from './types'; + +@Injectable() +export class IpLocatorService { + constructor(private readonly fetchService: HttpService) { } + + private api = 'https://ip-api.com/json'; + + /** + * @description 541151 is a number generated to include the + * following fields in the response: + * - status + * - Country info: country,countryCode + * - Region info: region,regionName + * - City info: city,district + * - Timezone and gepolocation: lat,lon,timezone + * @see https://ip-api.com/docs/api:json + */ + private fields = 541151; + + /** + * @param ip - IP of the client + * @returns API URL path to query from + */ + private getApi(ip: string) { + return `${this.api}/${ip}?fields=${this.fields}`; + } + + /** + * @param ip - IP of the client + * + * @description Provides geographic and location data associated with the IP + * @returns {IpLocationDto} + */ + async locate(ip: string): Promise { + const { data } = await firstValueFrom( + this.fetchService.get(this.getApi(ip)) + ); + return data; + } + + /** + * @description Formatted location data + * @example "Canada (CA) / Quebec" + */ + async getLocation(ip: string): Promise { + const locationData = await this.locate(ip); + return `${locationData.country} (${locationData.countryCode}) / ${locationData.regionName}`; + } + + /** + * @description Latitude and longitude coordinates + */ + async getGeolocation(ip: string): Promise { + const locationData = await this.locate(ip); + return { lon: locationData.lon, lat: locationData.lat }; + } +} diff --git a/libs/generics/ip-locator/src/lib/types.ts b/libs/generics/ip-locator/src/lib/types.ts new file mode 100644 index 0000000..f98d28a --- /dev/null +++ b/libs/generics/ip-locator/src/lib/types.ts @@ -0,0 +1,22 @@ + +export interface RegionalLocation { + region: string; + regionName: string; +} + +export interface CountryLocation { + country: string; + countryCode: string; +} + +export interface Geolocation { + lon: number; + lat: number; +} + +export interface IpLocationDto extends Geolocation, RegionalLocation, CountryLocation { + status: 'success' | 'fail'; + city: string; + district: string; + timezone: string; +} diff --git a/libs/generics/ip-locator/tsconfig.json b/libs/generics/ip-locator/tsconfig.json new file mode 100644 index 0000000..03d08bc --- /dev/null +++ b/libs/generics/ip-locator/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/generics/ip-locator/tsconfig.lib.json b/libs/generics/ip-locator/tsconfig.lib.json new file mode 100644 index 0000000..f8aeacc --- /dev/null +++ b/libs/generics/ip-locator/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/generics/ip-locator/tsconfig.spec.json b/libs/generics/ip-locator/tsconfig.spec.json new file mode 100644 index 0000000..adff492 --- /dev/null +++ b/libs/generics/ip-locator/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/generics/mail/.eslintrc.json b/libs/generics/mail/.eslintrc.json new file mode 100644 index 0000000..9761c56 --- /dev/null +++ b/libs/generics/mail/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/core/mail/README.md b/libs/generics/mail/README.md similarity index 74% rename from libs/core/mail/README.md rename to libs/generics/mail/README.md index be2dc45..1fdbcb7 100644 --- a/libs/core/mail/README.md +++ b/libs/generics/mail/README.md @@ -3,8 +3,9 @@ Mail Service provider for core module. Supported Mail providers: -- SMTP server using nodemailer -- resend.com integration + +- SMTP server using nodemailer +- resend.com integration ## Running unit tests diff --git a/libs/generics/mail/jest.config.ts b/libs/generics/mail/jest.config.ts new file mode 100644 index 0000000..fe2f22b --- /dev/null +++ b/libs/generics/mail/jest.config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +export default { + displayName: 'mail', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': [ + 'ts-jest', + { tsconfig: '/tsconfig.spec.json' }, + ], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/generics/mail', + passWithNoTests: true, +}; diff --git a/libs/generics/mail/project.json b/libs/generics/mail/project.json new file mode 100644 index 0000000..4c36f0d --- /dev/null +++ b/libs/generics/mail/project.json @@ -0,0 +1,9 @@ +{ + "name": "mail", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/generics/mail/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project mail --web", + "targets": {} +} diff --git a/libs/core/mail/src/index.ts b/libs/generics/mail/src/index.ts similarity index 100% rename from libs/core/mail/src/index.ts rename to libs/generics/mail/src/index.ts diff --git a/libs/core/mail/src/lib/application/dtos/index.ts b/libs/generics/mail/src/lib/application/dtos/index.ts similarity index 100% rename from libs/core/mail/src/lib/application/dtos/index.ts rename to libs/generics/mail/src/lib/application/dtos/index.ts diff --git a/libs/core/mail/src/lib/application/dtos/mail.dto.ts b/libs/generics/mail/src/lib/application/dtos/mail.dto.ts similarity index 100% rename from libs/core/mail/src/lib/application/dtos/mail.dto.ts rename to libs/generics/mail/src/lib/application/dtos/mail.dto.ts diff --git a/libs/core/mail/src/lib/application/index.ts b/libs/generics/mail/src/lib/application/index.ts similarity index 100% rename from libs/core/mail/src/lib/application/index.ts rename to libs/generics/mail/src/lib/application/index.ts diff --git a/libs/core/mail/src/lib/application/ports/index.ts b/libs/generics/mail/src/lib/application/ports/index.ts similarity index 100% rename from libs/core/mail/src/lib/application/ports/index.ts rename to libs/generics/mail/src/lib/application/ports/index.ts diff --git a/libs/core/mail/src/lib/application/ports/mail-provider.port.ts b/libs/generics/mail/src/lib/application/ports/mail-provider.port.ts similarity index 100% rename from libs/core/mail/src/lib/application/ports/mail-provider.port.ts rename to libs/generics/mail/src/lib/application/ports/mail-provider.port.ts diff --git a/libs/core/mail/src/lib/application/services/index.ts b/libs/generics/mail/src/lib/application/services/index.ts similarity index 100% rename from libs/core/mail/src/lib/application/services/index.ts rename to libs/generics/mail/src/lib/application/services/index.ts diff --git a/libs/core/mail/src/lib/application/services/mail.service.ts b/libs/generics/mail/src/lib/application/services/mail.service.ts similarity index 100% rename from libs/core/mail/src/lib/application/services/mail.service.ts rename to libs/generics/mail/src/lib/application/services/mail.service.ts diff --git a/libs/core/mail/src/lib/infrastructure/config-mail-provider-module.util.ts b/libs/generics/mail/src/lib/infrastructure/config-mail-provider-module.util.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/config-mail-provider-module.util.ts rename to libs/generics/mail/src/lib/infrastructure/config-mail-provider-module.util.ts diff --git a/libs/core/mail/src/lib/infrastructure/index.ts b/libs/generics/mail/src/lib/infrastructure/index.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/index.ts rename to libs/generics/mail/src/lib/infrastructure/index.ts diff --git a/libs/core/mail/src/lib/infrastructure/mail-modules.type.ts b/libs/generics/mail/src/lib/infrastructure/mail-modules.type.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/mail-modules.type.ts rename to libs/generics/mail/src/lib/infrastructure/mail-modules.type.ts diff --git a/libs/core/mail/src/lib/infrastructure/mailer/index.ts b/libs/generics/mail/src/lib/infrastructure/mailer/index.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/mailer/index.ts rename to libs/generics/mail/src/lib/infrastructure/mailer/index.ts diff --git a/libs/core/mail/src/lib/infrastructure/mailer/mailer.adapter.ts b/libs/generics/mail/src/lib/infrastructure/mailer/mailer.adapter.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/mailer/mailer.adapter.ts rename to libs/generics/mail/src/lib/infrastructure/mailer/mailer.adapter.ts diff --git a/libs/core/mail/src/lib/infrastructure/mailer/mailer.module-options.ts b/libs/generics/mail/src/lib/infrastructure/mailer/mailer.module-options.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/mailer/mailer.module-options.ts rename to libs/generics/mail/src/lib/infrastructure/mailer/mailer.module-options.ts diff --git a/libs/core/mail/src/lib/infrastructure/mailer/mailer.module.ts b/libs/generics/mail/src/lib/infrastructure/mailer/mailer.module.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/mailer/mailer.module.ts rename to libs/generics/mail/src/lib/infrastructure/mailer/mailer.module.ts diff --git a/libs/core/mail/src/lib/infrastructure/resend/index.ts b/libs/generics/mail/src/lib/infrastructure/resend/index.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/resend/index.ts rename to libs/generics/mail/src/lib/infrastructure/resend/index.ts diff --git a/libs/core/mail/src/lib/infrastructure/resend/resend.adapter.ts b/libs/generics/mail/src/lib/infrastructure/resend/resend.adapter.ts similarity index 99% rename from libs/core/mail/src/lib/infrastructure/resend/resend.adapter.ts rename to libs/generics/mail/src/lib/infrastructure/resend/resend.adapter.ts index a30b75f..576b6eb 100644 --- a/libs/core/mail/src/lib/infrastructure/resend/resend.adapter.ts +++ b/libs/generics/mail/src/lib/infrastructure/resend/resend.adapter.ts @@ -6,7 +6,7 @@ import { RequireAtLeastOne } from 'type-fest'; @Injectable() export class ResendMailProvider implements MailProvider { - constructor(private readonly resendService: ResendService) { } + constructor(private readonly resendService: ResendService) {} public send(mail: RequireAtLeastOne) { this.resendService.send({ diff --git a/libs/core/mail/src/lib/infrastructure/resend/resend.module-options.ts b/libs/generics/mail/src/lib/infrastructure/resend/resend.module-options.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/resend/resend.module-options.ts rename to libs/generics/mail/src/lib/infrastructure/resend/resend.module-options.ts diff --git a/libs/core/mail/src/lib/infrastructure/resend/resend.module.ts b/libs/generics/mail/src/lib/infrastructure/resend/resend.module.ts similarity index 100% rename from libs/core/mail/src/lib/infrastructure/resend/resend.module.ts rename to libs/generics/mail/src/lib/infrastructure/resend/resend.module.ts diff --git a/libs/core/mail/src/lib/mail.module.ts b/libs/generics/mail/src/lib/mail.module.ts similarity index 100% rename from libs/core/mail/src/lib/mail.module.ts rename to libs/generics/mail/src/lib/mail.module.ts diff --git a/libs/generics/mail/tsconfig.json b/libs/generics/mail/tsconfig.json new file mode 100644 index 0000000..03d08bc --- /dev/null +++ b/libs/generics/mail/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/generics/mail/tsconfig.lib.json b/libs/generics/mail/tsconfig.lib.json new file mode 100644 index 0000000..f8aeacc --- /dev/null +++ b/libs/generics/mail/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/generics/mail/tsconfig.spec.json b/libs/generics/mail/tsconfig.spec.json new file mode 100644 index 0000000..adff492 --- /dev/null +++ b/libs/generics/mail/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/shared/common/src/lib/api/index.ts b/libs/shared/common/src/lib/api/index.ts index 684a0b3..25358ef 100644 --- a/libs/shared/common/src/lib/api/index.ts +++ b/libs/shared/common/src/lib/api/index.ts @@ -2,3 +2,4 @@ export * from './response.base'; export * from './api-error.response'; export * from './paginated.response.base'; export * from './paginated-query.request.dto'; +export * from './user-agent.decorator'; \ No newline at end of file diff --git a/libs/shared/common/src/lib/api/response.base.ts b/libs/shared/common/src/lib/api/response.base.ts index 080298c..d376e61 100644 --- a/libs/shared/common/src/lib/api/response.base.ts +++ b/libs/shared/common/src/lib/api/response.base.ts @@ -2,6 +2,7 @@ export interface BaseResponseProps { message: string; code: string; success: boolean; + desc?: string; data?: TData; errors?: string[]; } @@ -25,6 +26,7 @@ export class ResponseBase implements BaseResponseProps readonly message: string; readonly success: boolean; readonly code: string; + readonly desc?: string; readonly data?: TData; readonly errors?: string[]; } diff --git a/libs/shared/common/src/lib/api/user-agent.decorator.ts b/libs/shared/common/src/lib/api/user-agent.decorator.ts new file mode 100644 index 0000000..373882d --- /dev/null +++ b/libs/shared/common/src/lib/api/user-agent.decorator.ts @@ -0,0 +1,10 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; + +export const UserAgent = createParamDecorator( + (data: unknown, ctx: ExecutionContext): string => { + const request: Request = ctx.switchToHttp().getRequest(); + const userAgent = request.headers['user-agent']; + return Array.isArray(userAgent) ? userAgent.join(', ') : userAgent || ''; + }, +); \ No newline at end of file diff --git a/libs/shared/common/src/lib/exceptions/exception.base.ts b/libs/shared/common/src/lib/exceptions/exception.base.ts index 252b641..754e2bd 100644 --- a/libs/shared/common/src/lib/exceptions/exception.base.ts +++ b/libs/shared/common/src/lib/exceptions/exception.base.ts @@ -4,7 +4,6 @@ export interface SerializedException { code: string; stack?: string; cause?: string; - metadata?: unknown; /** * ^ Consider adding optional `metadata` object to * exceptions (if language doesn't support anything @@ -12,6 +11,14 @@ export interface SerializedException { * information about the exception when throwing. * This will make debugging easier. */ + metadata?: unknown; + /** + * Success is indication of whether the operation succeed, + * or not. Since some errors can be invoked despite the fact + * the operation successfuly completed. + */ + success: boolean; + } /** @@ -46,8 +53,17 @@ export abstract class ExceptionBase extends Error { message: this.message, code: this.code, stack: this.stack, + success: false, cause: JSON.stringify(this.cause), metadata: this.metadata, }; } + + toClientError(): SerializedException { + return { + message: this.message, + code: this.code, + success: false, + }; + } } diff --git a/libs/shared/common/src/lib/types/index.ts b/libs/shared/common/src/lib/types/index.ts index a798b3c..9eea631 100644 --- a/libs/shared/common/src/lib/types/index.ts +++ b/libs/shared/common/src/lib/types/index.ts @@ -8,3 +8,4 @@ export * from './non-function-properties.type'; export * from './object-literal.type'; export * from './require-one.type'; export * from './mutable.type'; +export * from './optional'; diff --git a/libs/shared/common/src/lib/types/optional.ts b/libs/shared/common/src/lib/types/optional.ts new file mode 100644 index 0000000..0b1a3ab --- /dev/null +++ b/libs/shared/common/src/lib/types/optional.ts @@ -0,0 +1 @@ +export type Optional = T | undefined | null; diff --git a/libs/shared/config/.eslintrc.json b/libs/shared/config/.eslintrc.json new file mode 100644 index 0000000..9761c56 --- /dev/null +++ b/libs/shared/config/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/config/project.json b/libs/shared/config/project.json new file mode 100644 index 0000000..7bab9f8 --- /dev/null +++ b/libs/shared/config/project.json @@ -0,0 +1,9 @@ +{ + "name": "config", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/config/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project config --web", + "targets": {} +} diff --git a/libs/core/config/src/index.ts b/libs/shared/config/src/index.ts similarity index 100% rename from libs/core/config/src/index.ts rename to libs/shared/config/src/index.ts diff --git a/libs/core/config/src/schema.ts b/libs/shared/config/src/schema.ts similarity index 100% rename from libs/core/config/src/schema.ts rename to libs/shared/config/src/schema.ts diff --git a/libs/shared/config/tsconfig.json b/libs/shared/config/tsconfig.json new file mode 100644 index 0000000..3a69557 --- /dev/null +++ b/libs/shared/config/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/shared/config/tsconfig.lib.json b/libs/shared/config/tsconfig.lib.json new file mode 100644 index 0000000..2b245b0 --- /dev/null +++ b/libs/shared/config/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/shared/types/.eslintrc.json b/libs/shared/types/.eslintrc.json deleted file mode 100644 index 3cc6e3d..0000000 --- a/libs/shared/types/.eslintrc.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.json"], - "parser": "jsonc-eslint-parser", - "rules": { - "@nx/dependency-checks": [ - "error", - { - "ignoredFiles": ["{projectRoot}/vite.config.{js,ts,mjs,mts}"] - } - ] - } - } - ] -} diff --git a/libs/shared/types/.swcrc b/libs/shared/types/.swcrc deleted file mode 100644 index d54df2b..0000000 --- a/libs/shared/types/.swcrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "jsc": { - "target": "es2017", - "parser": { - "syntax": "typescript", - "decorators": true, - "dynamicImport": true - }, - "transform": { - "decoratorMetadata": true, - "legacyDecorator": true - }, - "keepClassNames": true, - "externalHelpers": true, - "loose": true - }, - "module": { - "type": "commonjs" - }, - "sourceMaps": true, - "exclude": [ - "jest.config.ts", - ".*\\.spec.tsx?$", - ".*\\.test.tsx?$", - "./src/jest-setup.ts$", - "./**/jest-setup.ts$", - ".*.js$" - ] -} diff --git a/libs/shared/types/README.md b/libs/shared/types/README.md deleted file mode 100644 index 2b23b56..0000000 --- a/libs/shared/types/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# types - -This library was generated with [Nx](https://nx.dev). - -## Building - -Run `nx build types` to build the library. - -## Running unit tests - -Run `nx test types` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/shared/types/package.json b/libs/shared/types/package.json deleted file mode 100644 index a44f357..0000000 --- a/libs/shared/types/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@goran/types", - "version": "0.0.1", - "dependencies": { - "@swc/helpers": "~0.5.2" - }, - "type": "commonjs", - "main": "./src/index.js", - "typings": "./src/index.d.ts", - "private": true -} diff --git a/libs/shared/types/project.json b/libs/shared/types/project.json deleted file mode 100644 index 4042a89..0000000 --- a/libs/shared/types/project.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "types", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/shared/types/src", - "projectType": "library", - "tags": [], - "targets": { - "build": { - "executor": "@nx/js:swc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared/types", - "main": "libs/shared/types/src/index.ts", - "tsConfig": "libs/shared/types/tsconfig.lib.json", - "assets": ["libs/shared/types/*.md"] - } - } - } -} diff --git a/libs/shared/types/tsconfig.json b/libs/shared/types/tsconfig.json deleted file mode 100644 index 8122543..0000000 --- a/libs/shared/types/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/shared/types/tsconfig.lib.json b/libs/shared/types/tsconfig.lib.json deleted file mode 100644 index 1f66ef5..0000000 --- a/libs/shared/types/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/shared/types/tsconfig.spec.json b/libs/shared/types/tsconfig.spec.json deleted file mode 100644 index 05a0e18..0000000 --- a/libs/shared/types/tsconfig.spec.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ] - }, - "include": [ - "vite.config.ts", - "vitest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ] -} diff --git a/libs/shared/types/vite.config.ts b/libs/shared/types/vite.config.ts deleted file mode 100644 index 7505f85..0000000 --- a/libs/shared/types/vite.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig } from 'vite'; - -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; - -export default defineConfig({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/libs/shared/types', - - plugins: [nxViteTsPaths()], - - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - - test: { - globals: true, - cache: { dir: '../../../node_modules/.vitest' }, - environment: 'node', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../../coverage/libs/shared/types', - provider: 'v8', - }, - }, -}); diff --git a/libs/shared/utils/src/lib/goran-banner.ts b/libs/shared/utils/src/lib/goran-banner.ts index 161e291..93be72d 100644 --- a/libs/shared/utils/src/lib/goran-banner.ts +++ b/libs/shared/utils/src/lib/goran-banner.ts @@ -1,9 +1,18 @@ -export const goranBanner = ` +export const goranBanner = ({ + appLink, + docsLink, +}: { + appLink: string; + docsLink: string; +}) => ` \t ______ _____ ______ _______ __ _ \t | ____ | | |_____/ |_____| | \\ | \t |_____| |_____| | \\_ | | | \\_| -\t Open Source Music Streaming Service +\t ⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺ +\t  Open Source Music Streaming Solution +\t  krr0ption/goran +\t  www.mardin.cc -\t ________________________________________ -\t Github: github.com/krr0ption/goran +\t  Application is running on: ${appLink} +\t  Documentation is running on: ${docsLink} `; diff --git a/package.json b/package.json index a033ac8..c8e255e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@nestjs-modules/mailer": "^2.0.2", + "@nestjs/axios": "^3.0.3", "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.3.10", "@nestjs/config": "^3.2.3", @@ -45,7 +46,7 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-router": "^1.48.1", - "axios": "^1.7.2", + "axios": "^1.7.7", "bcrypt": "^5.1.1", "cache-manager": "^5.7.4", "class-transformer": "^0.5.1", @@ -54,7 +55,8 @@ "clsx": "^2.1.1", "cmdk": "1.0.0", "cookie-parser": "^1.4.6", - "date-fns": "^4.0.0", + "date-fns": "^3.6.0", + "device-detector-js": "^3.0.3", "drizzle-orm": "^0.33.0", "ejs": "^3.1.10", "embla-carousel-react": "^8.1.8", @@ -64,17 +66,21 @@ "joi": "^17.13.3", "lucide-react": "^0.428.0", "nanoid": "^5.0.7", + "nestjs-pino": "^4.1.0", "nestjs-request-context": "^3.0.0", "nestjs-resend": "^0.0.3", "next": "14.2.10", "next-themes": "^0.3.0", + "node-device-detector": "^2.1.4", "nodemailer": "^6.9.14", "otp-generator": "^4.0.1", "oxide.ts": "^1.1.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pg": "^8.12.0", - "pg-mem": "^3.0.2", + "pg-mem": "^2.8.1", + "pino-http": "^10.3.0", + "pino-pretty": "^11.2.2", "pnpm": "8", "postgres": "^3.4.4", "react": "18.2.0", @@ -168,7 +174,8 @@ "ts-jest": "^29.2.3", "ts-node": "10.9.1", "typescript": "~5.4.5", - "vite": "~5.2.14", + "vite": "~5.0.13", + "vite-plugin-dts": "^4.2.1", "vitest": "^1.6.0", "webpack-cli": "^5.1.4" }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 615bebc..2ecb9d9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,34 +1,46 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "importHelpers": true, - "target": "es2015", - "module": "esnext", - "lib": ["es2020", "dom"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@goran/common": ["libs/shared/common/src/index.ts"], - "@goran/config": ["libs/core/config/src/index.ts"], - "@goran/drizzle-data-access": ["libs/data-access/drizzle/src/index.ts"], - "@goran/mail": ["libs/core/mail/src/index.ts"], - "@goran/security": ["libs/core/security/src/index.ts"], - "@goran/types": ["libs/shared/types/src/index.ts"], - "@goran/ui-common": ["libs/ui/common/src/index.ts"], - "@goran/ui-common/*": ["libs/libs/ui-common/src/*"], - "@goran/ui-components": ["libs/ui/components/src/index.ts"], - "@goran/ui-components/*": ["libs/libs/ui-components/src/*"], - "@goran/users": ["libs/domain/users/src/index.ts"], - "@goran/utils": ["libs/shared/utils/src/index.ts"] - } - }, - "exclude": ["node_modules", "tmp"] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "importHelpers": true, + "target": "es2015", + "module": "esnext", + "lib": ["es2020", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "strictNullChecks": true, + "strict": true, + "baseUrl": ".", + "paths": { + "@goran/brand": ["libs/ui/brand/src/index.ts"], + "@goran/common": ["libs/shared/common/src/index.ts"], + "@goran/config": ["libs/shared/config/src/index.ts"], + "@goran/device-detector": [ + "libs/generics/device-detector/src/index.ts" + ], + "@goran/drizzle-data-access": [ + "libs/data-access/drizzle/src/index.ts" + ], + "@goran/ip-locator": ["libs/generics/ip-locator/src/index.ts"], + "@goran/mail": ["libs/generics/mail/src/index.ts"], + "@goran/security": ["libs/core/security/src/index.ts"], + "@goran/ui-common": ["libs/ui/common/src/index.ts"], + "@goran/ui-common/*": ["libs/libs/ui-common/src/*"], + "@goran/ui-components": ["libs/ui/components/src/index.ts"], + "@goran/ui-components/*": ["libs/libs/ui-components/src/*"], + "@goran/ui-module-auth": ["libs/ui/modules/auth/src/index.ts"], + "@goran/ui-module-auth/server": [ + "libs/ui/modules/auth/src/server.ts" + ], + "@goran/users": ["libs/core/users/src/index.ts"], + "@goran/utils": ["libs/shared/utils/src/index.ts"] + } + }, + "exclude": ["node_modules", "tmp"] } diff --git a/yarn.lock b/yarn.lock index 691f928..9fa774c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1892,13 +1892,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/aix-ppc64@npm:0.20.2" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -1927,13 +1920,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/android-arm64@npm:0.20.2" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm64@npm:0.21.5" @@ -1962,13 +1948,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/android-arm@npm:0.20.2" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm@npm:0.21.5" @@ -1997,13 +1976,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/android-x64@npm:0.20.2" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-x64@npm:0.21.5" @@ -2032,13 +2004,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/darwin-arm64@npm:0.20.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-arm64@npm:0.21.5" @@ -2067,13 +2032,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/darwin-x64@npm:0.20.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-x64@npm:0.21.5" @@ -2102,13 +2060,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/freebsd-arm64@npm:0.20.2" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-arm64@npm:0.21.5" @@ -2137,13 +2088,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/freebsd-x64@npm:0.20.2" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-x64@npm:0.21.5" @@ -2172,13 +2116,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-arm64@npm:0.20.2" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm64@npm:0.21.5" @@ -2207,13 +2144,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-arm@npm:0.20.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm@npm:0.21.5" @@ -2242,13 +2172,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-ia32@npm:0.20.2" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ia32@npm:0.21.5" @@ -2277,13 +2200,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-loong64@npm:0.20.2" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-loong64@npm:0.21.5" @@ -2312,13 +2228,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-mips64el@npm:0.20.2" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-mips64el@npm:0.21.5" @@ -2347,13 +2256,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-ppc64@npm:0.20.2" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ppc64@npm:0.21.5" @@ -2382,13 +2284,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-riscv64@npm:0.20.2" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-riscv64@npm:0.21.5" @@ -2417,13 +2312,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-s390x@npm:0.20.2" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-s390x@npm:0.21.5" @@ -2452,13 +2340,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-x64@npm:0.20.2" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-x64@npm:0.21.5" @@ -2487,13 +2368,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/netbsd-x64@npm:0.20.2" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/netbsd-x64@npm:0.21.5" @@ -2529,13 +2403,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/openbsd-x64@npm:0.20.2" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/openbsd-x64@npm:0.21.5" @@ -2564,13 +2431,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/sunos-x64@npm:0.20.2" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -2599,13 +2459,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/win32-arm64@npm:0.20.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-arm64@npm:0.21.5" @@ -2634,13 +2487,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/win32-ia32@npm:0.20.2" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-ia32@npm:0.21.5" @@ -2669,13 +2515,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/win32-x64@npm:0.20.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-x64@npm:0.21.5" @@ -2887,6 +2726,7 @@ __metadata: "@faker-js/faker": "npm:^8.4.1" "@hookform/resolvers": "npm:^3.9.0" "@nestjs-modules/mailer": "npm:^2.0.2" + "@nestjs/axios": "npm:^3.0.3" "@nestjs/cache-manager": "npm:^2.2.2" "@nestjs/common": "npm:^10.3.10" "@nestjs/config": "npm:^3.2.3" @@ -2966,7 +2806,7 @@ __metadata: "@vitest/coverage-v8": "npm:^1.6.0" "@vitest/ui": "npm:^2.0.5" autoprefixer: "npm:10.4.13" - axios: "npm:^1.7.2" + axios: "npm:^1.7.7" babel-jest: "npm:^29.4.1" bcrypt: "npm:^5.1.1" cache-manager: "npm:^5.7.4" @@ -2977,7 +2817,8 @@ __metadata: cmdk: "npm:1.0.0" cookie-parser: "npm:^1.4.6" cypress: "npm:^13.13.1" - date-fns: "npm:^4.0.0" + date-fns: "npm:^3.6.0" + device-detector-js: "npm:^3.0.3" drizzle-kit: "npm:^0.24.2" drizzle-orm: "npm:^0.33.0" ejs: "npm:^3.1.10" @@ -3001,10 +2842,12 @@ __metadata: lucide-react: "npm:^0.428.0" madge: "npm:^7.0.0" nanoid: "npm:^5.0.7" + nestjs-pino: "npm:^4.1.0" nestjs-request-context: "npm:^3.0.0" nestjs-resend: "npm:^0.0.3" next: "npm:14.2.10" next-themes: "npm:^0.3.0" + node-device-detector: "npm:^2.1.4" nodemailer: "npm:^6.9.14" nx: "npm:18.2.4" otp-generator: "npm:^4.0.1" @@ -3012,7 +2855,9 @@ __metadata: passport: "npm:^0.7.0" passport-jwt: "npm:^4.0.1" pg: "npm:^8.12.0" - pg-mem: "npm:^3.0.2" + pg-mem: "npm:^2.8.1" + pino-http: "npm:^10.3.0" + pino-pretty: "npm:^11.2.2" pnpm: "npm:8" postcss: "npm:^8.4.40" postgres: "npm:^3.4.4" @@ -3039,7 +2884,8 @@ __metadata: typescript: "npm:~5.4.5" ulid: "npm:^2.3.0" vaul: "npm:^0.9.1" - vite: "npm:~5.2.14" + vite: "npm:~5.0.13" + vite-plugin-dts: "npm:^4.2.1" vitest: "npm:^1.6.0" webpack-cli: "npm:^5.1.4" zod: "npm:^3.23.8" @@ -3499,7 +3345,53 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc@npm:^0.15.0": +"@microsoft/api-extractor-model@npm:7.29.6": + version: 7.29.6 + resolution: "@microsoft/api-extractor-model@npm:7.29.6" + dependencies: + "@microsoft/tsdoc": "npm:~0.15.0" + "@microsoft/tsdoc-config": "npm:~0.17.0" + "@rushstack/node-core-library": "npm:5.7.0" + checksum: 10c0/cdf7e69dc6bc04de4a814b2bc126cc4faa5cea5b9b4582f8baf9bd9bd105cc62d5e322c46c6221a8e0934d1539a820ef9f512c112cb16c1bbcc7b06f5e3ba5fd + languageName: node + linkType: hard + +"@microsoft/api-extractor@npm:7.47.7": + version: 7.47.7 + resolution: "@microsoft/api-extractor@npm:7.47.7" + dependencies: + "@microsoft/api-extractor-model": "npm:7.29.6" + "@microsoft/tsdoc": "npm:~0.15.0" + "@microsoft/tsdoc-config": "npm:~0.17.0" + "@rushstack/node-core-library": "npm:5.7.0" + "@rushstack/rig-package": "npm:0.5.3" + "@rushstack/terminal": "npm:0.14.0" + "@rushstack/ts-command-line": "npm:4.22.6" + lodash: "npm:~4.17.15" + minimatch: "npm:~3.0.3" + resolve: "npm:~1.22.1" + semver: "npm:~7.5.4" + source-map: "npm:~0.6.1" + typescript: "npm:5.4.2" + bin: + api-extractor: bin/api-extractor + checksum: 10c0/f1121b8a2bd559c84d0397ce504561d138e7f1b6e56a0dbd9f71dadfbc8b129f34c512dbfc5f6554b4ca92df9fe00c47e0d7e175b9d6e02726b88ed84878e431 + languageName: node + linkType: hard + +"@microsoft/tsdoc-config@npm:~0.17.0": + version: 0.17.0 + resolution: "@microsoft/tsdoc-config@npm:0.17.0" + dependencies: + "@microsoft/tsdoc": "npm:0.15.0" + ajv: "npm:~8.12.0" + jju: "npm:~1.4.0" + resolve: "npm:~1.22.2" + checksum: 10c0/9aa51b5b0fa93ad5c6a40ed1acf1f25c625b616efe29f2e5fa22ee9bddea12a4a39c833726e11ab592f20cfc9b8c3865978864dd02711d457fa971df3c091847 + languageName: node + linkType: hard + +"@microsoft/tsdoc@npm:0.15.0, @microsoft/tsdoc@npm:^0.15.0, @microsoft/tsdoc@npm:~0.15.0": version: 0.15.0 resolution: "@microsoft/tsdoc@npm:0.15.0" checksum: 10c0/6beaf6e01ff54daeba69862cb3d27e03bbabfe299d23d0fade885f5b29bf98af01cecc746d23875fe60ba89514e3b630b71140b1b18d37301096f7a1e35451aa @@ -3584,6 +3476,17 @@ __metadata: languageName: node linkType: hard +"@nestjs/axios@npm:^3.0.3": + version: 3.0.3 + resolution: "@nestjs/axios@npm:3.0.3" + peerDependencies: + "@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + axios: ^1.3.1 + rxjs: ^6.0.0 || ^7.0.0 + checksum: 10c0/15320400f11375ab9a7b13b50159623e23f33bb0987ea555eaddc9d9b6ef6fcd815b57743179b957449c76e9ad6491398791f846be28a99a38ff108557ad03e3 + languageName: node + linkType: hard + "@nestjs/cache-manager@npm:^2.2.2": version: 2.2.2 resolution: "@nestjs/cache-manager@npm:2.2.2" @@ -9348,6 +9251,22 @@ __metadata: languageName: node linkType: hard +"@rollup/pluginutils@npm:^5.1.0": + version: 5.1.0 + resolution: "@rollup/pluginutils@npm:5.1.0" + dependencies: + "@types/estree": "npm:^1.0.0" + estree-walker: "npm:^2.0.2" + picomatch: "npm:^2.3.1" + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/c7bed15711f942d6fdd3470fef4105b73991f99a478605e13d41888963330a6f9e32be37e6ddb13f012bc7673ff5e54f06f59fd47109436c1c513986a8a7612d + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-android-arm-eabi@npm:4.21.0" @@ -9355,6 +9274,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.21.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-android-arm64@npm:4.21.0" @@ -9362,6 +9288,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-android-arm64@npm:4.21.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-darwin-arm64@npm:4.21.0" @@ -9369,6 +9302,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.21.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-darwin-x64@npm:4.21.0" @@ -9376,6 +9316,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.21.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.21.0" @@ -9383,6 +9330,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.21.3" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.21.0" @@ -9390,6 +9344,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.21.3" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.21.0" @@ -9397,6 +9358,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.21.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.21.0" @@ -9404,6 +9372,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.21.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.0" @@ -9411,6 +9386,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.21.0" @@ -9418,6 +9400,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.21.3" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.21.0" @@ -9425,6 +9414,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.21.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.21.0" @@ -9432,6 +9428,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.21.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-linux-x64-musl@npm:4.21.0" @@ -9439,6 +9442,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.21.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.21.0" @@ -9446,6 +9456,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.21.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.21.0" @@ -9453,6 +9470,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.21.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.21.0" @@ -9460,6 +9484,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.21.3": + version: 4.21.3 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.21.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -9474,6 +9505,64 @@ __metadata: languageName: node linkType: hard +"@rushstack/node-core-library@npm:5.7.0": + version: 5.7.0 + resolution: "@rushstack/node-core-library@npm:5.7.0" + dependencies: + ajv: "npm:~8.13.0" + ajv-draft-04: "npm:~1.0.0" + ajv-formats: "npm:~3.0.1" + fs-extra: "npm:~7.0.1" + import-lazy: "npm:~4.0.0" + jju: "npm:~1.4.0" + resolve: "npm:~1.22.1" + semver: "npm:~7.5.4" + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/8055c52a9658942a77d78afc5e6d6c6779e8f083024ec741b868da198ccd821a52e7566e848dade2fc8570c235210e114d53fe79d51aaf8d1649960828f73679 + languageName: node + linkType: hard + +"@rushstack/rig-package@npm:0.5.3": + version: 0.5.3 + resolution: "@rushstack/rig-package@npm:0.5.3" + dependencies: + resolve: "npm:~1.22.1" + strip-json-comments: "npm:~3.1.1" + checksum: 10c0/ef0b0115b60007f965b875f671019ac7fc26592f6bf7d7b40fa8c68e8dc37e9f7dcda3b5533b489ebf04d28a182dc60987bfd365a8d4173c73d482b270647741 + languageName: node + linkType: hard + +"@rushstack/terminal@npm:0.14.0": + version: 0.14.0 + resolution: "@rushstack/terminal@npm:0.14.0" + dependencies: + "@rushstack/node-core-library": "npm:5.7.0" + supports-color: "npm:~8.1.1" + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/9fda5bc7d2c74d9ef52021163acb4a6ff26cc28e4935567c149158a01966051313d6785b5cf2dd9238404181b1766f253153ba0a5aaea4fb9f5c5bb9d708a10b + languageName: node + linkType: hard + +"@rushstack/ts-command-line@npm:4.22.6": + version: 4.22.6 + resolution: "@rushstack/ts-command-line@npm:4.22.6" + dependencies: + "@rushstack/terminal": "npm:0.14.0" + "@types/argparse": "npm:1.0.38" + argparse: "npm:~1.0.9" + string-argv: "npm:~0.3.1" + checksum: 10c0/44905de94dbfe4a9b27ce21fb09b50f92a637349989a61a9d0fee53f38f81d5b779e8cfa8f8a857d0d415db91557b687159522d6b3ecdaa3eccbbff76e608325 + languageName: node + linkType: hard + "@selderee/plugin-htmlparser2@npm:^0.10.0": version: 0.10.0 resolution: "@selderee/plugin-htmlparser2@npm:0.10.0" @@ -10112,6 +10201,13 @@ __metadata: languageName: node linkType: hard +"@types/argparse@npm:1.0.38": + version: 1.0.38 + resolution: "@types/argparse@npm:1.0.38" + checksum: 10c0/4fc892da5df16923f48180da2d1f4562fa8b0507cf636b24780444fa0a1d7321d4dc0c0ecbee6152968823f5a2ae0d321b4f8c705a489bf1ae1245bdeb0868fd + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.4 resolution: "@types/aria-query@npm:5.0.4" @@ -11229,6 +11325,94 @@ __metadata: languageName: node linkType: hard +"@volar/language-core@npm:2.4.4, @volar/language-core@npm:~2.4.1": + version: 2.4.4 + resolution: "@volar/language-core@npm:2.4.4" + dependencies: + "@volar/source-map": "npm:2.4.4" + checksum: 10c0/455c088c7e6cd0583633300f8f3a6a2d78ee35b7cb15401516f4fde8cab41cbc5b55b5855f9627e2840d7af6fc4a74d175039bdde96b8c27e8f949441bc06670 + languageName: node + linkType: hard + +"@volar/source-map@npm:2.4.4": + version: 2.4.4 + resolution: "@volar/source-map@npm:2.4.4" + checksum: 10c0/c5875ecb1961108e6c24d26add81355057517f2a874f64f1abc65fd0d90fb3b6feebd5a8305a76e0089948a124b2ec7d889ade51bade3341baf3e3b7e118c8e6 + languageName: node + linkType: hard + +"@volar/typescript@npm:^2.4.4": + version: 2.4.4 + resolution: "@volar/typescript@npm:2.4.4" + dependencies: + "@volar/language-core": "npm:2.4.4" + path-browserify: "npm:^1.0.1" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/07d404aa7024ff8b8231b5c21460b990344beb2179dd772931811f3107a2ddd1ff9ba02446cb30c6699a9ea0868cf76af4027e3168f1761ba8a0013def9ebf96 + languageName: node + linkType: hard + +"@vue/compiler-core@npm:3.5.4": + version: 3.5.4 + resolution: "@vue/compiler-core@npm:3.5.4" + dependencies: + "@babel/parser": "npm:^7.25.3" + "@vue/shared": "npm:3.5.4" + entities: "npm:^4.5.0" + estree-walker: "npm:^2.0.2" + source-map-js: "npm:^1.2.0" + checksum: 10c0/78be2129758e12b3a92d47a96f3f0d414632da3738366cf5c3eba57d626dbe02e9f313c145fb6bba35993fc783909f9e943f2d5305a446ad2e81988779d0c7bd + languageName: node + linkType: hard + +"@vue/compiler-dom@npm:^3.4.0": + version: 3.5.4 + resolution: "@vue/compiler-dom@npm:3.5.4" + dependencies: + "@vue/compiler-core": "npm:3.5.4" + "@vue/shared": "npm:3.5.4" + checksum: 10c0/149bd09cc40a4bb71239f0e2ece4ee7013889fe5363930757484a6e3d398f7f55edb658c1113c6cb7a1ad6d9689d91fac1dc971187d7971e8990ca28703a2634 + languageName: node + linkType: hard + +"@vue/compiler-vue2@npm:^2.7.16": + version: 2.7.16 + resolution: "@vue/compiler-vue2@npm:2.7.16" + dependencies: + de-indent: "npm:^1.0.2" + he: "npm:^1.2.0" + checksum: 10c0/c76c3fad770b9a7da40b314116cc9da173da20e5fd68785c8ed8dd8a87d02f239545fa296e16552e040ec86b47bfb18283b39447b250c2e76e479bd6ae475bb3 + languageName: node + linkType: hard + +"@vue/language-core@npm:2.1.6": + version: 2.1.6 + resolution: "@vue/language-core@npm:2.1.6" + dependencies: + "@volar/language-core": "npm:~2.4.1" + "@vue/compiler-dom": "npm:^3.4.0" + "@vue/compiler-vue2": "npm:^2.7.16" + "@vue/shared": "npm:^3.4.0" + computeds: "npm:^0.0.1" + minimatch: "npm:^9.0.3" + muggle-string: "npm:^0.4.1" + path-browserify: "npm:^1.0.1" + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/bad09d54929f09d0d809f13ac1a3ccf0ab0d848c11c420e83a951f7fecfe15537caf95fc55756770a4d79f1fa6b4488bf2846afaba6854746fbb349cbb294bed + languageName: node + linkType: hard + +"@vue/shared@npm:3.5.4, @vue/shared@npm:^3.4.0": + version: 3.5.4 + resolution: "@vue/shared@npm:3.5.4" + checksum: 10c0/e8f8be133edd371e1cc421b1f74fe59865f48813276785ff657b33be8c4b485f033aef5cb0542883ef61b437661d5fbf90264657291889db389d64b668d1e656 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1": version: 1.12.1 resolution: "@webassemblyjs/ast@npm:1.12.1" @@ -11487,6 +11671,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -11587,6 +11780,18 @@ __metadata: languageName: node linkType: hard +"ajv-draft-04@npm:~1.0.0": + version: 1.0.0 + resolution: "ajv-draft-04@npm:1.0.0" + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/6044310bd38c17d77549fd326bd40ce1506fa10b0794540aa130180808bf94117fac8c9b448c621512bea60e4a947278f6a978e87f10d342950c15b33ddd9271 + languageName: node + linkType: hard + "ajv-formats@npm:2.1.1, ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -11601,6 +11806,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:~3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a + languageName: node + linkType: hard + "ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -11621,7 +11840,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.12.0": +"ajv@npm:8.12.0, ajv@npm:~8.12.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -11657,6 +11876,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:~8.13.0": + version: 8.13.0 + resolution: "ajv@npm:8.13.0" + dependencies: + fast-deep-equal: "npm:^3.1.3" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + uri-js: "npm:^4.4.1" + checksum: 10c0/14c6497b6f72843986d7344175a1aa0e2c35b1e7f7475e55bc582cddb765fca7e6bf950f465dc7846f817776d9541b706f4b5b3fbedd8dfdeb5fce6f22864264 + languageName: node + linkType: hard + "alce@npm:1.2.0": version: 1.2.0 resolution: "alce@npm:1.2.0" @@ -11807,7 +12038,7 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": +"argparse@npm:^1.0.7, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" dependencies: @@ -12062,6 +12293,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10c0/e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a + languageName: node + linkType: hard + "autoprefixer@npm:10.4.13": version: 10.4.13 resolution: "autoprefixer@npm:10.4.13" @@ -12128,7 +12366,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.0, axios@npm:^1.7.2": +"axios@npm:^1.6.0": version: 1.7.4 resolution: "axios@npm:1.7.4" dependencies: @@ -12139,7 +12377,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.4": +"axios@npm:^1.7.4, axios@npm:^1.7.7": version: 1.7.7 resolution: "axios@npm:1.7.7" dependencies: @@ -12615,6 +12853,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + languageName: node + linkType: hard + "busboy@npm:1.6.0, busboy@npm:^1.0.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" @@ -13201,7 +13449,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10, colorette@npm:^2.0.14, colorette@npm:^2.0.16, colorette@npm:^2.0.20": +"colorette@npm:^2.0.10, colorette@npm:^2.0.14, colorette@npm:^2.0.16, colorette@npm:^2.0.20, colorette@npm:^2.0.7": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 @@ -13296,6 +13544,13 @@ __metadata: languageName: node linkType: hard +"compare-versions@npm:^6.1.1": + version: 6.1.1 + resolution: "compare-versions@npm:6.1.1" + checksum: 10c0/415205c7627f9e4f358f571266422980c9fe2d99086be0c9a48008ef7c771f32b0fbe8e97a441ffedc3910872f917a0675fe0fe3c3b6d331cda6d8690be06338 + languageName: node + linkType: hard + "component-emitter@npm:^1.3.0": version: 1.3.1 resolution: "component-emitter@npm:1.3.1" @@ -13334,6 +13589,13 @@ __metadata: languageName: node linkType: hard +"computeds@npm:^0.0.1": + version: 0.0.1 + resolution: "computeds@npm:0.0.1" + checksum: 10c0/8a8736f1f43e4a99286519785d71a10ece8f444a2fa1fc2fe1f03dedf63f3477b45094002c85a2826f7631759c9f5a00b4ace47456997f253073fc525e8983de + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -14082,10 +14344,17 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^4.0.0": - version: 4.0.0 - resolution: "date-fns@npm:4.0.0" - checksum: 10c0/c706d29f05c28fdaea939068d688489bbc3096ef2486d1fe4a56d209c3a061ba99febae943b9562bb8ac08fe61257cf11fe74adad164adb7ba6df994e09cb99b +"date-fns@npm:^3.6.0": + version: 3.6.0 + resolution: "date-fns@npm:3.6.0" + checksum: 10c0/0b5fb981590ef2f8e5a3ba6cd6d77faece0ea7f7158948f2eaae7bbb7c80a8f63ae30b01236c2923cf89bb3719c33aeb150c715ea4fe4e86e37dcf06bed42fb6 + languageName: node + linkType: hard + +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10c0/e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6 languageName: node linkType: hard @@ -14096,6 +14365,13 @@ __metadata: languageName: node linkType: hard +"de-indent@npm:^1.0.2": + version: 1.0.2 + resolution: "de-indent@npm:1.0.2" + checksum: 10c0/7058ce58abd6dfc123dd204e36be3797abd419b59482a634605420f47ae97639d0c183ec5d1b904f308a01033f473673897afc2bd59bc620ebf1658763ef4291 + languageName: node + linkType: hard + "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -14126,6 +14402,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.6": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b + languageName: node + linkType: hard + "decimal.js-light@npm:^2.4.1": version: 2.5.1 resolution: "decimal.js-light@npm:2.5.1" @@ -14451,6 +14739,13 @@ __metadata: languageName: node linkType: hard +"device-detector-js@npm:^3.0.3": + version: 3.0.3 + resolution: "device-detector-js@npm:3.0.3" + checksum: 10c0/421d333d151b9c8205d1db61247603a4ecbdff5f627d72f1acbd98b17fc07b7a076bcde6f5d3e07727f17593ccf448462917cece6555646cfc9c0422a1692dbd + languageName: node + linkType: hard + "dezalgo@npm:^1.0.4": version: 1.0.4 resolution: "dezalgo@npm:1.0.4" @@ -15221,118 +15516,38 @@ __metadata: dependencies: debug: "npm:^4.3.4" peerDependencies: - esbuild: ">=0.12 <1" - checksum: 10c0/77193b7ca32ba9f81b35ddf3d3d0138efb0b1429d71b39480cfee932e1189dd2e492bd32bf04a4d0bc3adfbc7ec7381ceb5ffd06efe35f3e70904f1f686566d5 - languageName: node - linkType: hard - -"esbuild@npm:^0.19.7": - version: 0.19.12 - resolution: "esbuild@npm:0.19.12" - dependencies: - "@esbuild/aix-ppc64": "npm:0.19.12" - "@esbuild/android-arm": "npm:0.19.12" - "@esbuild/android-arm64": "npm:0.19.12" - "@esbuild/android-x64": "npm:0.19.12" - "@esbuild/darwin-arm64": "npm:0.19.12" - "@esbuild/darwin-x64": "npm:0.19.12" - "@esbuild/freebsd-arm64": "npm:0.19.12" - "@esbuild/freebsd-x64": "npm:0.19.12" - "@esbuild/linux-arm": "npm:0.19.12" - "@esbuild/linux-arm64": "npm:0.19.12" - "@esbuild/linux-ia32": "npm:0.19.12" - "@esbuild/linux-loong64": "npm:0.19.12" - "@esbuild/linux-mips64el": "npm:0.19.12" - "@esbuild/linux-ppc64": "npm:0.19.12" - "@esbuild/linux-riscv64": "npm:0.19.12" - "@esbuild/linux-s390x": "npm:0.19.12" - "@esbuild/linux-x64": "npm:0.19.12" - "@esbuild/netbsd-x64": "npm:0.19.12" - "@esbuild/openbsd-x64": "npm:0.19.12" - "@esbuild/sunos-x64": "npm:0.19.12" - "@esbuild/win32-arm64": "npm:0.19.12" - "@esbuild/win32-ia32": "npm:0.19.12" - "@esbuild/win32-x64": "npm:0.19.12" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/0f2d21ffe24ebead64843f87c3aebe2e703a5ed9feb086a0728b24907fac2eb9923e4a79857d3df9059c915739bd7a870dd667972eae325c67f478b592b8582d + esbuild: ">=0.12 <1" + checksum: 10c0/77193b7ca32ba9f81b35ddf3d3d0138efb0b1429d71b39480cfee932e1189dd2e492bd32bf04a4d0bc3adfbc7ec7381ceb5ffd06efe35f3e70904f1f686566d5 languageName: node linkType: hard -"esbuild@npm:^0.20.1": - version: 0.20.2 - resolution: "esbuild@npm:0.20.2" - dependencies: - "@esbuild/aix-ppc64": "npm:0.20.2" - "@esbuild/android-arm": "npm:0.20.2" - "@esbuild/android-arm64": "npm:0.20.2" - "@esbuild/android-x64": "npm:0.20.2" - "@esbuild/darwin-arm64": "npm:0.20.2" - "@esbuild/darwin-x64": "npm:0.20.2" - "@esbuild/freebsd-arm64": "npm:0.20.2" - "@esbuild/freebsd-x64": "npm:0.20.2" - "@esbuild/linux-arm": "npm:0.20.2" - "@esbuild/linux-arm64": "npm:0.20.2" - "@esbuild/linux-ia32": "npm:0.20.2" - "@esbuild/linux-loong64": "npm:0.20.2" - "@esbuild/linux-mips64el": "npm:0.20.2" - "@esbuild/linux-ppc64": "npm:0.20.2" - "@esbuild/linux-riscv64": "npm:0.20.2" - "@esbuild/linux-s390x": "npm:0.20.2" - "@esbuild/linux-x64": "npm:0.20.2" - "@esbuild/netbsd-x64": "npm:0.20.2" - "@esbuild/openbsd-x64": "npm:0.20.2" - "@esbuild/sunos-x64": "npm:0.20.2" - "@esbuild/win32-arm64": "npm:0.20.2" - "@esbuild/win32-ia32": "npm:0.20.2" - "@esbuild/win32-x64": "npm:0.20.2" +"esbuild@npm:^0.19.3, esbuild@npm:^0.19.7": + version: 0.19.12 + resolution: "esbuild@npm:0.19.12" + dependencies: + "@esbuild/aix-ppc64": "npm:0.19.12" + "@esbuild/android-arm": "npm:0.19.12" + "@esbuild/android-arm64": "npm:0.19.12" + "@esbuild/android-x64": "npm:0.19.12" + "@esbuild/darwin-arm64": "npm:0.19.12" + "@esbuild/darwin-x64": "npm:0.19.12" + "@esbuild/freebsd-arm64": "npm:0.19.12" + "@esbuild/freebsd-x64": "npm:0.19.12" + "@esbuild/linux-arm": "npm:0.19.12" + "@esbuild/linux-arm64": "npm:0.19.12" + "@esbuild/linux-ia32": "npm:0.19.12" + "@esbuild/linux-loong64": "npm:0.19.12" + "@esbuild/linux-mips64el": "npm:0.19.12" + "@esbuild/linux-ppc64": "npm:0.19.12" + "@esbuild/linux-riscv64": "npm:0.19.12" + "@esbuild/linux-s390x": "npm:0.19.12" + "@esbuild/linux-x64": "npm:0.19.12" + "@esbuild/netbsd-x64": "npm:0.19.12" + "@esbuild/openbsd-x64": "npm:0.19.12" + "@esbuild/sunos-x64": "npm:0.19.12" + "@esbuild/win32-arm64": "npm:0.19.12" + "@esbuild/win32-ia32": "npm:0.19.12" + "@esbuild/win32-x64": "npm:0.19.12" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -15382,7 +15597,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10c0/66398f9fb2c65e456a3e649747b39af8a001e47963b25e86d9c09d2a48d61aa641b27da0ce5cad63df95ad246105e1d83e7fee0e1e22a0663def73b1c5101112 + checksum: 10c0/0f2d21ffe24ebead64843f87c3aebe2e703a5ed9feb086a0728b24907fac2eb9923e4a79857d3df9059c915739bd7a870dd667972eae325c67f478b592b8582d languageName: node linkType: hard @@ -16198,6 +16413,13 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^2.0.2": + version: 2.0.2 + resolution: "estree-walker@npm:2.0.2" + checksum: 10c0/53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af + languageName: node + linkType: hard + "estree-walker@npm:^3.0.3": version: 3.0.3 resolution: "estree-walker@npm:3.0.3" @@ -16221,6 +16443,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b + languageName: node + linkType: hard + "eventemitter2@npm:6.4.7": version: 6.4.7 resolution: "eventemitter2@npm:6.4.7" @@ -16249,7 +16478,7 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.2.0": +"events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 @@ -16524,6 +16753,13 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-copy@npm:3.0.2" + checksum: 10c0/02e8b9fd03c8c024d2987760ce126456a0e17470850b51e11a1c3254eed6832e4733ded2d93316c82bc0b36aeb991ad1ff48d1ba95effe7add7c3ab8d8eb554a + languageName: node + linkType: hard + "fast-deep-equal@npm:^2.0.1": version: 2.0.1 resolution: "fast-deep-equal@npm:2.0.1" @@ -16585,6 +16821,13 @@ __metadata: languageName: node linkType: hard +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: 10c0/7e2ce4aad6e7535e0775bf12bd3e4f2e53d8051d8b630e0fa9e67f68cb0b0e6070d2f7a94b1d0522ef07e32f7c7cda5755e2b677a6538f1e9070ca053c42343a + languageName: node + linkType: hard + "fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -17077,6 +17320,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:~7.0.1": + version: 7.0.1 + resolution: "fs-extra@npm:7.0.1" + dependencies: + graceful-fs: "npm:^4.1.2" + jsonfile: "npm:^4.0.0" + universalify: "npm:^0.1.0" + checksum: 10c0/1943bb2150007e3739921b8d13d4109abdc3cc481e53b97b7ea7f77eda1c3c642e27ae49eac3af074e3496ea02fde30f411ef410c760c70a38b92e656e5da784 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -17642,6 +17896,13 @@ __metadata: languageName: node linkType: hard +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10c0/054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb + languageName: node + linkType: hard + "hexoid@npm:^1.0.0": version: 1.0.0 resolution: "hexoid@npm:1.0.0" @@ -18038,6 +18299,13 @@ __metadata: languageName: node linkType: hard +"import-lazy@npm:~4.0.0": + version: 4.0.0 + resolution: "import-lazy@npm:4.0.0" + checksum: 10c0/a3520313e2c31f25c0b06aa66d167f329832b68a4f957d7c9daf6e0fa41822b6e84948191648b9b9d8ca82f94740cdf15eecf2401a5b42cd1c33fd84f2225cca + languageName: node + linkType: hard + "import-local@npm:^3.0.2": version: 3.2.0 resolution: "import-local@npm:3.2.0" @@ -19316,6 +19584,13 @@ __metadata: languageName: node linkType: hard +"jju@npm:~1.4.0": + version: 1.4.0 + resolution: "jju@npm:1.4.0" + checksum: 10c0/f3f444557e4364cfc06b1abf8331bf3778b26c0c8552ca54429bc0092652172fdea26cbffe33e1017b303d5aa506f7ede8571857400efe459cb7439180e2acad + languageName: node + linkType: hard + "joi@npm:^17.13.3": version: 17.13.3 resolution: "joi@npm:17.13.3" @@ -19329,6 +19604,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae + languageName: node + linkType: hard + "js-beautify@npm:^1.14.11, js-beautify@npm:^1.6.12, js-beautify@npm:^1.6.14": version: 1.15.1 resolution: "js-beautify@npm:1.15.1" @@ -19616,6 +19898,18 @@ __metadata: languageName: node linkType: hard +"jsonfile@npm:^4.0.0": + version: 4.0.0 + resolution: "jsonfile@npm:4.0.0" + dependencies: + graceful-fs: "npm:^4.1.6" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10c0/7dc94b628d57a66b71fb1b79510d460d662eb975b5f876d723f81549c2e9cd316d58a2ddf742b2b93a4fa6b17b2accaf1a738a0e2ea114bdfb13a32e5377e480 + languageName: node + linkType: hard + "jsonfile@npm:^6.0.1": version: 6.1.0 resolution: "jsonfile@npm:6.1.0" @@ -19763,6 +20057,13 @@ __metadata: languageName: node linkType: hard +"kolorist@npm:^1.8.0": + version: 1.8.0 + resolution: "kolorist@npm:1.8.0" + checksum: 10c0/73075db44a692bf6c34a649f3b4b3aea4993b84f6b754cbf7a8577e7c7db44c0bad87752bd23b0ce533f49de2244ce2ce03b7b1b667a85ae170a94782cc50f9b + languageName: node + linkType: hard + "language-subtag-registry@npm:^0.3.20": version: 0.3.23 resolution: "language-subtag-registry@npm:0.3.23" @@ -20184,7 +20485,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.21": +"lodash@npm:4.17.21, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.21, lodash@npm:~4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -20364,7 +20665,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.5": +"magic-string@npm:^0.30.11, magic-string@npm:^0.30.5": version: 0.30.11 resolution: "magic-string@npm:0.30.11" dependencies: @@ -20691,6 +20992,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:~3.0.3": + version: 3.0.8 + resolution: "minimatch@npm:3.0.8" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/72b226f452dcfb5075255f53534cb83fc25565b909e79b9be4fad463d735cb1084827f7013ff41d050e77ee6e474408c6073473edd2fb72c2fd630cfb0acc6ad + languageName: node + linkType: hard + "minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -21278,13 +21588,20 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 languageName: node linkType: hard +"muggle-string@npm:^0.4.1": + version: 0.4.1 + resolution: "muggle-string@npm:0.4.1" + checksum: 10c0/e914b63e24cd23f97e18376ec47e4ba3aa24365e4776212b666add2e47bb158003212980d732c49abf3719568900af7861873844a6e2d3a7ca7e86952c0e99e9 + languageName: node + linkType: hard + "multer@npm:1.4.4-lts.1": version: 1.4.4-lts.1 resolution: "multer@npm:1.4.4-lts.1" @@ -21391,6 +21708,16 @@ __metadata: languageName: node linkType: hard +"nestjs-pino@npm:^4.1.0": + version: 4.1.0 + resolution: "nestjs-pino@npm:4.1.0" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/003c21588454e2e9c1f7f7cc1829aec005acaca45f73b29b33da11c20479af811c72089dcdd03e08bdd675373c6563f288124b8566824936bde8f7991bd86c9b + languageName: node + linkType: hard + "nestjs-request-context@npm:^3.0.0": version: 3.0.0 resolution: "nestjs-request-context@npm:3.0.0" @@ -21526,6 +21853,15 @@ __metadata: languageName: node linkType: hard +"node-device-detector@npm:^2.1.4": + version: 2.1.4 + resolution: "node-device-detector@npm:2.1.4" + dependencies: + js-yaml: "npm:^4.1.0" + checksum: 10c0/da39bbaeb86d309fef04ac3c97c72ce0ce5922006bac4d23f342c2b1b85a7adba471adcdd45e4f54775c72586cb6e76949158093d71469f6b72d3432556c3ba8 + languageName: node + linkType: hard + "node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -22010,6 +22346,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10c0/faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570 + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -22422,6 +22765,13 @@ __metadata: languageName: node linkType: hard +"path-browserify@npm:^1.0.1": + version: 1.0.1 + resolution: "path-browserify@npm:1.0.1" + checksum: 10c0/8b8c3fd5c66bd340272180590ae4ff139769e9ab79522e2eb82e3d571a89b8117c04147f65ad066dccfb42fcad902e5b7d794b3d35e0fd840491a8ddbedf8c66 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -22586,9 +22936,9 @@ __metadata: languageName: node linkType: hard -"pg-mem@npm:^3.0.2": - version: 3.0.2 - resolution: "pg-mem@npm:3.0.2" +"pg-mem@npm:^2.8.1": + version: 2.9.1 + resolution: "pg-mem@npm:2.9.1" dependencies: functional-red-black-tree: "npm:^1.0.1" immutable: "npm:^4.3.4" @@ -22603,8 +22953,6 @@ __metadata: knex: ">=0.20" kysely: ">=0.26" pg-promise: ">=10.8.7" - pg-server: ^0.1.5 - postgres: ^3.4.4 slonik: ">=23.0.1" typeorm: ">=0.2.29" peerDependenciesMeta: @@ -22620,15 +22968,11 @@ __metadata: optional: true pg-promise: optional: true - pg-server: - optional: true - postgres: - optional: true slonik: optional: true typeorm: optional: true - checksum: 10c0/09d26b2b7067ac348fc2450d137da87690b720fee26308afeb172787b503bb76b306883b764ffc911cabfc8f648e06bb0d1ab6798e2e04a4511213f1f9aeed80 + checksum: 10c0/ee5f2598f68d7e2e877bf0269c4657863ae81c1f3f6bd9cd976b0913a8897f55e45e4c6c5fe542a63c2f33f846b3e68e5301f2f0ecfde54aa1c6e19202dbfaac languageName: node linkType: hard @@ -22766,6 +23110,80 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:^1.2.0": + version: 1.2.0 + resolution: "pino-abstract-transport@npm:1.2.0" + dependencies: + readable-stream: "npm:^4.0.0" + split2: "npm:^4.0.0" + checksum: 10c0/b4ab59529b7a91f488440147fc58ee0827a6c1c5ca3627292339354b1381072c1a6bfa9b46d03ad27872589e8477ecf74da12cf286e1e6b665ac64a3b806bf07 + languageName: node + linkType: hard + +"pino-http@npm:^10.3.0": + version: 10.3.0 + resolution: "pino-http@npm:10.3.0" + dependencies: + get-caller-file: "npm:^2.0.5" + pino: "npm:^9.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^4.0.0" + checksum: 10c0/da95d93e1176c02201f9b9bb0af53ad737105c5772acbb077dcad0f52ebce2438e0e9fc8216cd96396d1305d0ecf1f1d23142c7a50110a701ea093b2ee999ea7 + languageName: node + linkType: hard + +"pino-pretty@npm:^11.2.2": + version: 11.2.2 + resolution: "pino-pretty@npm:11.2.2" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.2" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.0.0" + pump: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + secure-json-parse: "npm:^2.4.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^3.1.1" + bin: + pino-pretty: bin.js + checksum: 10c0/3ce1769907886a5584f6c8123d9bc987712ad10a375797733a0fe95a238df587dac8e2b709bab291c4e30d41b0cf65808c708c96f8eb98b2778b6df60afa7e66 + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10c0/73e694d542e8de94445a03a98396cf383306de41fd75ecc07085d57ed7a57896198508a0dec6eefad8d701044af21eb27253ccc352586a03cf0d4a0bd25b4133 + languageName: node + linkType: hard + +"pino@npm:^9.0.0": + version: 9.4.0 + resolution: "pino@npm:9.4.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.2.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^4.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10c0/12a3d74968964d92b18ca7d6095a3c5b86478dc22264a37486d64e102085ed08820fcbe75e640acc3542fdf2937a34e5050b624f98e6ac62dd10f5e1328058a2 + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.6": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -23319,7 +23737,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.38": +"postcss@npm:^8.4.32": version: 8.4.47 resolution: "postcss@npm:8.4.47" dependencies: @@ -23540,6 +23958,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^4.0.0": + version: 4.0.0 + resolution: "process-warning@npm:4.0.0" + checksum: 10c0/5312a72b69d37a1b82ad03f3dfa0090dab3804a8fd995d06c28e3c002852bd82f5584217d9f4a3f197892bb2afc22d57e2c662c7e906b5abb48c0380c7b0880d + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -23845,6 +24270,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -24172,6 +24604,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10c0/a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" @@ -24190,6 +24635,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10c0/23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0 + languageName: node + linkType: hard + "recharts-scale@npm:^0.4.4": version: 0.4.5 resolution: "recharts-scale@npm:0.4.5" @@ -24466,7 +24918,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.20.0, resolve@npm:^1.22.2, resolve@npm:^1.22.3, resolve@npm:^1.22.4": +"resolve@npm:^1.1.7, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.20.0, resolve@npm:^1.22.2, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:~1.22.1, resolve@npm:~1.22.2": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -24492,7 +24944,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A~1.22.1#optional!builtin, resolve@patch:resolve@npm%3A~1.22.2#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -24646,6 +25098,69 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.2.0": + version: 4.21.3 + resolution: "rollup@npm:4.21.3" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.21.3" + "@rollup/rollup-android-arm64": "npm:4.21.3" + "@rollup/rollup-darwin-arm64": "npm:4.21.3" + "@rollup/rollup-darwin-x64": "npm:4.21.3" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.21.3" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.21.3" + "@rollup/rollup-linux-arm64-gnu": "npm:4.21.3" + "@rollup/rollup-linux-arm64-musl": "npm:4.21.3" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.21.3" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.21.3" + "@rollup/rollup-linux-s390x-gnu": "npm:4.21.3" + "@rollup/rollup-linux-x64-gnu": "npm:4.21.3" + "@rollup/rollup-linux-x64-musl": "npm:4.21.3" + "@rollup/rollup-win32-arm64-msvc": "npm:4.21.3" + "@rollup/rollup-win32-ia32-msvc": "npm:4.21.3" + "@rollup/rollup-win32-x64-msvc": "npm:4.21.3" + "@types/estree": "npm:1.0.5" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/a9f98366a451f1302276390de9c0c59b464d680946410f53c14e7057fa84642efbe05eca8d85076962657955d77bb4a2d2b6dd8b70baf58c3c4b56f565d804dd + languageName: node + linkType: hard + "rrweb-cssom@npm:^0.6.0": version: 0.6.0 resolution: "rrweb-cssom@npm:0.6.0" @@ -24724,6 +25239,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10c0/baea14971858cadd65df23894a40588ed791769db21bafb7fd7608397dbdce9c5aac60748abae9995e0fc37e15f2061980501e012cd48859740796bea2987f49 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -24851,6 +25373,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.4.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + "selderee@npm:^0.10.0": version: 0.10.0 resolution: "selderee@npm:0.10.0" @@ -24929,6 +25458,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:~7.5.4": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 10c0/5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -25266,6 +25806,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.1.0 + resolution: "sonic-boom@npm:4.1.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10c0/4c9e082db296fbfb02e22a1a9b8de8b82f5965697dda3fe7feadc4759bf25d1de0094e3c35f16e015bfdc00fad7b8cf15bef5b0144501a2a5c5b86efb5684096 + languageName: node + linkType: hard + "sonner@npm:^1.5.0": version: 1.5.0 resolution: "sonner@npm:1.5.0" @@ -25392,7 +25941,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^4.1.0": +"split2@npm:^4.0.0, split2@npm:^4.1.0": version: 4.2.0 resolution: "split2@npm:4.2.0" checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 @@ -25505,6 +26054,13 @@ __metadata: languageName: node linkType: hard +"string-argv@npm:~0.3.1": + version: 0.3.2 + resolution: "string-argv@npm:0.3.2" + checksum: 10c0/75c02a83759ad1722e040b86823909d9a2fc75d15dd71ec4b537c3560746e33b5f5a07f7332d1e3f88319909f82190843aa2f0a0d8c8d591ec08e93d5b8dec82 + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -25611,7 +26167,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -25693,7 +26249,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:^3.1.1, strip-json-comments@npm:~3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd @@ -25886,7 +26442,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1, supports-color@npm:~8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -26113,6 +26669,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10c0/c36118379940b77a6ef3e6f4d5dd31e97b8210c3f7b9a54eb8fe6358ab173f6d0acfaf69b9c3db024b948c0c5fd2a7df93e2e49151af02076b35ada3205ec9a6 + languageName: node + linkType: hard + "throttleit@npm:^1.0.0": version: 1.0.1 resolution: "throttleit@npm:1.0.1" @@ -26624,6 +27189,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:5.4.2": + version: 5.4.2 + resolution: "typescript@npm:5.4.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/583ff68cafb0c076695f72d61df6feee71689568179fb0d3a4834dac343df6b6ed7cf7b6f6c801fa52d43cd1d324e2f2d8ae4497b09f9e6cfe3d80a6d6c9ca52 + languageName: node + linkType: hard + "typescript@npm:^5.0.4, typescript@npm:^5.4.4": version: 5.5.4 resolution: "typescript@npm:5.5.4" @@ -26644,6 +27219,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A5.4.2#optional!builtin": + version: 5.4.2 + resolution: "typescript@patch:typescript@npm%3A5.4.2#optional!builtin::version=5.4.2&hash=5adc0c" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/fcf6658073d07283910d9a0e04b1d5d0ebc822c04dbb7abdd74c3151c7aa92fcddbac7d799404e358197222006ccdc4c0db219d223d2ee4ccd9e2b01333b49be + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.4.4#optional!builtin": version: 5.5.4 resolution: "typescript@patch:typescript@npm%3A5.5.4#optional!builtin::version=5.5.4&hash=379a07" @@ -26782,6 +27367,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.1.0": + version: 0.1.2 + resolution: "universalify@npm:0.1.2" + checksum: 10c0/e70e0339f6b36f34c9816f6bf9662372bd241714dc77508d231d08386d94f2c4aa1ba1318614f92015f40d45aae1b9075cd30bd490efbe39387b60a76ca3f045 + languageName: node + linkType: hard + "universalify@npm:^0.2.0": version: 0.2.0 resolution: "universalify@npm:0.2.0" @@ -26843,7 +27435,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -27082,6 +27674,29 @@ __metadata: languageName: node linkType: hard +"vite-plugin-dts@npm:^4.2.1": + version: 4.2.1 + resolution: "vite-plugin-dts@npm:4.2.1" + dependencies: + "@microsoft/api-extractor": "npm:7.47.7" + "@rollup/pluginutils": "npm:^5.1.0" + "@volar/typescript": "npm:^2.4.4" + "@vue/language-core": "npm:2.1.6" + compare-versions: "npm:^6.1.1" + debug: "npm:^4.3.6" + kolorist: "npm:^1.8.0" + local-pkg: "npm:^0.5.0" + magic-string: "npm:^0.30.11" + peerDependencies: + typescript: "*" + vite: "*" + peerDependenciesMeta: + vite: + optional: true + checksum: 10c0/cd18f7ee82ae93d56c90cd723fe93dfd169f4f35b80af520c9fb68d8eae84ee866329ac099fd7944f86c20752662c2aef380e784a9828f591eaa2df7ebf048c7 + languageName: node + linkType: hard + "vite@npm:^5.0.0": version: 5.4.1 resolution: "vite@npm:5.4.1" @@ -27125,14 +27740,14 @@ __metadata: languageName: node linkType: hard -"vite@npm:~5.2.14": - version: 5.2.14 - resolution: "vite@npm:5.2.14" +"vite@npm:~5.0.13": + version: 5.0.13 + resolution: "vite@npm:5.0.13" dependencies: - esbuild: "npm:^0.20.1" + esbuild: "npm:^0.19.3" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.38" - rollup: "npm:^4.13.0" + postcss: "npm:^8.4.32" + rollup: "npm:^4.2.0" peerDependencies: "@types/node": ^18.0.0 || >=20.0.0 less: "*" @@ -27161,7 +27776,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/0ed7a8f8274d14bbd01be2ca5c7c539f915e75d884a97f6051cdf494997832bc02c7db9fc9c5ba8f057d5fece28a3bf215761815e6014e843abe2c38a9424fb7 + checksum: 10c0/3c926f21b27379742a182c6594629ef5287fac2860e5f35ce744da35f35c3a967e822fb9b24d62a0f67a5fccca29b82d7982fbfc5208a58bfef31de7a8d499a4 languageName: node linkType: hard @@ -27222,6 +27837,13 @@ __metadata: languageName: node linkType: hard +"vscode-uri@npm:^3.0.8": + version: 3.0.8 + resolution: "vscode-uri@npm:3.0.8" + checksum: 10c0/f7f217f526bf109589969fe6e66b71e70b937de1385a1d7bb577ca3ee7c5e820d3856a86e9ff2fa9b7a0bc56a3dd8c3a9a557d3fedd7df414bc618d5e6b567f9 + languageName: node + linkType: hard + "w3c-xmlserializer@npm:^4.0.0": version: 4.0.0 resolution: "w3c-xmlserializer@npm:4.0.0"