From 1d354533389000b64defd38593a60fb51ca73e6b Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 9 Jun 2026 10:31:49 +0300 Subject: [PATCH] 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], + ); } }