From 6096867644f04d28bfbfac6b4051c4e3b1e4e71f Mon Sep 17 00:00:00 2001 From: Senior Engineer Date: Wed, 25 Mar 2026 01:50:53 +0100 Subject: [PATCH] feat(api): merchant registration, login, and JWT validation - Add MerchantsService with in-memory store (ready to swap for DB) - POST /auth/register and POST /auth/login with bcrypt password hashing - GET /auth/me for authenticated merchant profile - Wire JwtStrategy to verify merchant_id exists - Global ValidationPipe for DTO validation - Unit tests for AuthService and AuthController Closes #5 Made-with: Cursor --- apps/api/package.json | 4 + apps/api/src/auth/auth.controller.spec.ts | 64 +++++++++ apps/api/src/auth/auth.controller.ts | 38 +++++ apps/api/src/auth/auth.module.ts | 24 +++- apps/api/src/auth/auth.service.spec.ts | 76 ++++++++++ apps/api/src/auth/auth.service.ts | 51 +++++++ apps/api/src/auth/dto/login.dto.ts | 12 ++ apps/api/src/auth/dto/register.dto.ts | 17 +++ .../interfaces/auth-response.interface.ts | 9 ++ apps/api/src/auth/strategies/jwt.strategy.ts | 13 +- apps/api/src/main.ts | 8 ++ .../interfaces/merchant-record.interface.ts | 7 + apps/api/src/merchants/merchants.module.ts | 8 ++ apps/api/src/merchants/merchants.service.ts | 44 ++++++ pnpm-lock.yaml | 135 ++++++++++++++---- 15 files changed, 475 insertions(+), 35 deletions(-) create mode 100644 apps/api/src/auth/auth.controller.spec.ts create mode 100644 apps/api/src/auth/auth.controller.ts create mode 100644 apps/api/src/auth/auth.service.spec.ts create mode 100644 apps/api/src/auth/auth.service.ts create mode 100644 apps/api/src/auth/dto/login.dto.ts create mode 100644 apps/api/src/auth/dto/register.dto.ts create mode 100644 apps/api/src/auth/interfaces/auth-response.interface.ts create mode 100644 apps/api/src/merchants/interfaces/merchant-record.interface.ts create mode 100644 apps/api/src/merchants/merchants.module.ts create mode 100644 apps/api/src/merchants/merchants.service.ts diff --git a/apps/api/package.json b/apps/api/package.json index 9605cb5..22d5912 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -22,11 +22,15 @@ "dependencies": { "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", "@stellar/stellar-sdk": "^14.6.1", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", diff --git a/apps/api/src/auth/auth.controller.spec.ts b/apps/api/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..cfbdc3c --- /dev/null +++ b/apps/api/src/auth/auth.controller.spec.ts @@ -0,0 +1,64 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { MerchantsService } from '../merchants/merchants.service'; + +describe('AuthController', () => { + let controller: AuthController; + let authService: jest.Mocked>; + let merchantsService: jest.Mocked>; + + beforeEach(async () => { + authService = { + register: jest.fn(), + login: jest.fn(), + }; + merchantsService = { + findById: jest.fn(), + toPublicProfile: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { provide: AuthService, useValue: authService }, + { provide: MerchantsService, useValue: merchantsService }, + ], + }).compile(); + + controller = module.get(AuthController); + }); + + it('register delegates to AuthService', async () => { + const dto = { email: 'a@b.com', password: 'password12', name: 'Shop' }; + const expected = { + access_token: 't', + expires_in: 100, + merchant: { id: '1', email: 'a@b.com', name: 'Shop' }, + }; + authService.register.mockResolvedValue(expected); + await expect(controller.register(dto)).resolves.toEqual(expected); + expect(authService.register).toHaveBeenCalledWith(dto); + }); + + it('me returns public profile when merchant exists', async () => { + const record = { + id: 'mid', + email: 'm@x.com', + passwordHash: 'h', + name: 'M', + createdAt: new Date(), + }; + merchantsService.findById.mockResolvedValue(record); + merchantsService.toPublicProfile.mockReturnValue({ + id: 'mid', + email: 'm@x.com', + name: 'M', + }); + await expect(controller.me({ merchant_id: 'mid' })).resolves.toEqual({ + id: 'mid', + email: 'm@x.com', + name: 'M', + }); + }); +}); diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts new file mode 100644 index 0000000..88b5aa4 --- /dev/null +++ b/apps/api/src/auth/auth.controller.ts @@ -0,0 +1,38 @@ +import { Body, Controller, Get, NotFoundException, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthResponse } from './interfaces/auth-response.interface'; +import { Public } from './decorators/public.decorator'; +import { RegisterDto } from './dto/register.dto'; +import { LoginDto } from './dto/login.dto'; +import { CurrentMerchant } from './decorators/current-merchant.decorator'; +import type { MerchantUser } from './interfaces/merchant-user.interface'; +import { MerchantsService } from '../merchants/merchants.service'; + +@Controller('auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly merchantsService: MerchantsService, + ) {} + + @Public() + @Post('register') + register(@Body() dto: RegisterDto): Promise { + return this.authService.register(dto); + } + + @Public() + @Post('login') + login(@Body() dto: LoginDto): Promise { + return this.authService.login(dto); + } + + @Get('me') + async me(@CurrentMerchant() user: MerchantUser) { + const merchant = await this.merchantsService.findById(user.merchant_id); + if (!merchant) { + throw new NotFoundException('Merchant not found'); + } + return this.merchantsService.toPublicProfile(merchant); + } +} diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 1e6621b..34ea1cb 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -1,11 +1,29 @@ import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; -import { JwtStrategy } from './strategies/jwt.strategy'; +import { MerchantsModule } from '../merchants/merchants.module'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { JwtStrategy } from './strategies/jwt.strategy'; + +function jwtExpiresSec(): number { + const raw = process.env.JWT_EXPIRES_IN_SEC; + const n = raw ? Number.parseInt(raw, 10) : NaN; + return Number.isFinite(n) && n > 0 ? n : 60 * 60 * 24 * 7; +} @Module({ - imports: [PassportModule.register({ defaultStrategy: 'jwt' })], - providers: [JwtStrategy, JwtAuthGuard], + imports: [ + MerchantsModule, + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.register({ + secret: process.env.JWT_SECRET ?? 'default-secret-change-me', + signOptions: { expiresIn: jwtExpiresSec() }, + }), + ], + controllers: [AuthController], + providers: [JwtStrategy, JwtAuthGuard, AuthService], exports: [JwtAuthGuard], }) export class AuthModule {} diff --git a/apps/api/src/auth/auth.service.spec.ts b/apps/api/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..d901e06 --- /dev/null +++ b/apps/api/src/auth/auth.service.spec.ts @@ -0,0 +1,76 @@ +import { ConflictException, UnauthorizedException } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MerchantsModule } from '../merchants/merchants.module'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + MerchantsModule, + JwtModule.register({ + secret: 'unit-test-secret', + signOptions: { expiresIn: 3600 }, + }), + ], + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('registers a merchant and returns a JWT-shaped response', async () => { + const res = await service.register({ + email: 'merchant@example.com', + password: 'password12', + name: 'Test Shop', + }); + expect(res.access_token).toBeDefined(); + expect(res.expires_in).toBeGreaterThan(0); + expect(res.merchant.email).toBe('merchant@example.com'); + expect(res.merchant.name).toBe('Test Shop'); + }); + + it('rejects duplicate registration email', async () => { + await service.register({ + email: 'dup@example.com', + password: 'password12', + name: 'A', + }); + await expect( + service.register({ + email: 'dup@example.com', + password: 'otherpass1', + name: 'B', + }), + ).rejects.toBeInstanceOf(ConflictException); + }); + + it('logs in with correct credentials', async () => { + await service.register({ + email: 'login@example.com', + password: 'password12', + name: 'Login Co', + }); + const res = await service.login({ + email: 'login@example.com', + password: 'password12', + }); + expect(res.access_token).toBeDefined(); + expect(res.merchant.name).toBe('Login Co'); + }); + + it('rejects login with wrong password', async () => { + await service.register({ + email: 'bad@example.com', + password: 'password12', + name: 'X', + }); + await expect( + service.login({ email: 'bad@example.com', password: 'wrongpass' }), + ).rejects.toBeInstanceOf(UnauthorizedException); + }); +}); diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts new file mode 100644 index 0000000..46ced40 --- /dev/null +++ b/apps/api/src/auth/auth.service.ts @@ -0,0 +1,51 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import * as bcrypt from 'bcryptjs'; +import { MerchantsService } from '../merchants/merchants.service'; +import { MerchantRecord } from '../merchants/interfaces/merchant-record.interface'; +import { RegisterDto } from './dto/register.dto'; +import { LoginDto } from './dto/login.dto'; +import { AuthResponse } from './interfaces/auth-response.interface'; + +const BCRYPT_ROUNDS = 10; + +function jwtExpiresSec(): number { + const raw = process.env.JWT_EXPIRES_IN_SEC; + const n = raw ? Number.parseInt(raw, 10) : NaN; + return Number.isFinite(n) && n > 0 ? n : 60 * 60 * 24 * 7; +} + +@Injectable() +export class AuthService { + private readonly expiresInSec = jwtExpiresSec(); + + constructor( + private readonly merchantsService: MerchantsService, + private readonly jwtService: JwtService, + ) {} + + async register(dto: RegisterDto): Promise { + const passwordHash = await bcrypt.hash(dto.password, BCRYPT_ROUNDS); + const merchant = await this.merchantsService.create(dto.email, passwordHash, dto.name); + return this.buildAuthResponse(merchant); + } + + async login(dto: LoginDto): Promise { + const merchant = await this.merchantsService.findByEmail(dto.email); + if (!merchant || !(await bcrypt.compare(dto.password, merchant.passwordHash))) { + throw new UnauthorizedException('Invalid credentials'); + } + return this.buildAuthResponse(merchant); + } + + private async buildAuthResponse(merchant: MerchantRecord): Promise { + const access_token = await this.jwtService.signAsync({ + merchant_id: merchant.id, + }); + return { + access_token, + expires_in: this.expiresInSec, + merchant: this.merchantsService.toPublicProfile(merchant), + }; + } +} diff --git a/apps/api/src/auth/dto/login.dto.ts b/apps/api/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..5622234 --- /dev/null +++ b/apps/api/src/auth/dto/login.dto.ts @@ -0,0 +1,12 @@ +import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator'; + +export class LoginDto { + @IsEmail() + @MaxLength(255) + email!: string; + + @IsString() + @MinLength(1) + @MaxLength(128) + password!: string; +} diff --git a/apps/api/src/auth/dto/register.dto.ts b/apps/api/src/auth/dto/register.dto.ts new file mode 100644 index 0000000..6447c1c --- /dev/null +++ b/apps/api/src/auth/dto/register.dto.ts @@ -0,0 +1,17 @@ +import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator'; + +export class RegisterDto { + @IsEmail() + @MaxLength(255) + email!: string; + + @IsString() + @MinLength(8) + @MaxLength(128) + password!: string; + + @IsString() + @MinLength(1) + @MaxLength(200) + name!: string; +} diff --git a/apps/api/src/auth/interfaces/auth-response.interface.ts b/apps/api/src/auth/interfaces/auth-response.interface.ts new file mode 100644 index 0000000..b1b1889 --- /dev/null +++ b/apps/api/src/auth/interfaces/auth-response.interface.ts @@ -0,0 +1,9 @@ +export interface AuthResponse { + access_token: string; + expires_in: number; + merchant: { + id: string; + email: string; + name: string; + }; +} diff --git a/apps/api/src/auth/strategies/jwt.strategy.ts b/apps/api/src/auth/strategies/jwt.strategy.ts index c1cf0f6..324d7db 100644 --- a/apps/api/src/auth/strategies/jwt.strategy.ts +++ b/apps/api/src/auth/strategies/jwt.strategy.ts @@ -1,12 +1,13 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { MerchantsService } from '../../merchants/merchants.service'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; import { MerchantUser } from '../interfaces/merchant-user.interface'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { + constructor(private readonly merchantsService: MerchantsService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, @@ -19,11 +20,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException('Invalid token: merchant_id missing'); } - // TODO: Validate merchant exists in database - // Example: - // const merchant = await this.merchantService.findById(payload.merchant_id); - // if (!merchant) throw new UnauthorizedException('Merchant not found'); + const merchant = await this.merchantsService.findById(payload.merchant_id); + if (!merchant) { + throw new UnauthorizedException('Merchant not found'); + } - return { merchant_id: payload.merchant_id }; + return { merchant_id: merchant.id }; } } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index f76bc8d..e63c62f 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,8 +1,16 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/apps/api/src/merchants/interfaces/merchant-record.interface.ts b/apps/api/src/merchants/interfaces/merchant-record.interface.ts new file mode 100644 index 0000000..cb6bb6c --- /dev/null +++ b/apps/api/src/merchants/interfaces/merchant-record.interface.ts @@ -0,0 +1,7 @@ +export interface MerchantRecord { + id: string; + email: string; + passwordHash: string; + name: string; + createdAt: Date; +} diff --git a/apps/api/src/merchants/merchants.module.ts b/apps/api/src/merchants/merchants.module.ts new file mode 100644 index 0000000..17bc9bc --- /dev/null +++ b/apps/api/src/merchants/merchants.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { MerchantsService } from './merchants.service'; + +@Module({ + providers: [MerchantsService], + exports: [MerchantsService], +}) +export class MerchantsModule {} diff --git a/apps/api/src/merchants/merchants.service.ts b/apps/api/src/merchants/merchants.service.ts new file mode 100644 index 0000000..97fe9bd --- /dev/null +++ b/apps/api/src/merchants/merchants.service.ts @@ -0,0 +1,44 @@ +import { ConflictException, Injectable } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import { MerchantRecord } from './interfaces/merchant-record.interface'; + +@Injectable() +export class MerchantsService { + private readonly byId = new Map(); + private readonly byEmail = new Map(); + + async create(email: string, passwordHash: string, name: string): Promise { + const normalized = email.toLowerCase().trim(); + if (this.byEmail.has(normalized)) { + throw new ConflictException('Email already registered'); + } + const id = randomUUID(); + const record: MerchantRecord = { + id, + email: normalized, + passwordHash, + name: name.trim(), + createdAt: new Date(), + }; + this.byId.set(id, record); + this.byEmail.set(normalized, record); + return record; + } + + async findByEmail(email: string): Promise { + const normalized = email.toLowerCase().trim(); + return this.byEmail.get(normalized) ?? null; + } + + async findById(id: string): Promise { + return this.byId.get(id) ?? null; + } + + toPublicProfile(record: MerchantRecord): { id: string; email: string; name: string } { + return { + id: record.id, + email: record.email, + name: record.name, + }; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de8c444..8644d2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,25 +85,37 @@ importers: dependencies: '@nestjs/common': specifier: ^11.0.1 - version: 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 - version: 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/passport': specifier: ^11.0.5 - version: 11.0.5(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + version: 11.0.5(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-express': specifier: ^11.0.1 - version: 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) '@nestjs/terminus': specifier: ^11.1.1 - version: 11.1.1(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/throttler': specifier: ^6.5.0 - version: 6.5.0(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2) + version: 6.5.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2) '@stellar/stellar-sdk': specifier: ^14.6.1 version: 14.6.1 + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.15.1 + version: 0.15.1 passport: specifier: ^0.7.0 version: 0.7.0 @@ -131,7 +143,7 @@ importers: version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.1 - version: 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14) '@types/express': specifier: ^5.0.0 version: 5.0.6 @@ -2221,6 +2233,14 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.2': + resolution: + { + integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==, + } + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/passport@11.0.5': resolution: { @@ -4946,6 +4966,12 @@ packages: integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==, } + '@types/validator@13.15.10': + resolution: + { + integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==, + } + '@types/yargs-parser@21.0.3': resolution: { @@ -5739,6 +5765,13 @@ packages: engines: { node: '>=6.0.0' } hasBin: true + bcryptjs@3.0.3: + resolution: + { + integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==, + } + hasBin: true + bignumber.js@9.3.1: resolution: { @@ -5982,6 +6015,18 @@ packages: integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==, } + class-transformer@0.5.1: + resolution: + { + integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==, + } + + class-validator@0.15.1: + resolution: + { + integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==, + } + class-variance-authority@0.7.1: resolution: { @@ -8664,6 +8709,12 @@ packages: } engines: { node: '>= 0.8.0' } + libphonenumber-js@1.12.40: + resolution: + { + integrity: sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==, + } + lightningcss-android-arm64@1.31.1: resolution: { @@ -11496,6 +11547,13 @@ packages: } engines: { node: ^20.17.0 || >=22.9.0 } + validator@13.15.26: + resolution: + { + integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==, + } + engines: { node: '>= 0.10' } + vary@1.1.2: resolution: { @@ -13045,7 +13103,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.3.0 iterare: 1.2.1 @@ -13054,12 +13112,15 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -13069,17 +13130,23 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) - '@nestjs/passport@11.0.5(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 + + '@nestjs/passport@11.0.5(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-express@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': + '@nestjs/platform-express@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 multer: 2.0.2 @@ -13099,27 +13166,27 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 - '@nestjs/testing@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14)': + '@nestjs/testing@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14)': dependencies: - '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) - '@nestjs/throttler@6.5.0(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 '@next/env@16.1.6': {} @@ -15001,6 +15068,8 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} + '@types/validator@13.15.10': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -15518,6 +15587,8 @@ snapshots: baseline-browser-mapping@2.10.0: {} + bcryptjs@3.0.3: {} + bignumber.js@9.3.1: {} bl@4.1.0: @@ -15663,6 +15734,14 @@ snapshots: cjs-module-lexer@2.2.0: {} + class-transformer@0.5.1: {} + + class-validator@0.15.1: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.40 + validator: 13.15.26 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -17536,6 +17615,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.12.40: {} + lightningcss-android-arm64@1.31.1: optional: true @@ -19388,6 +19469,8 @@ snapshots: validate-npm-package-name@7.0.2: {} + validator@13.15.26: {} + vary@1.1.2: {} vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):