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 }; }); } async logout(user: any, req: any) { const iamAuthService = await this.resolveIamAuthService(req); await iamAuthService.logout(user); 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]); 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); } } }