refactor( iam ): remove prisma.user references from passenger-side services

This commit is contained in:
Abubeker Yasin
2026-06-09 10:31:49 +03:00
parent 32f2f4b917
commit 1d35453338
9 changed files with 99 additions and 107 deletions

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Passenger" ADD COLUMN "blockedUntil" TIMESTAMP(3);
-- RenameIndex
ALTER INDEX "UserPreferences_userId_key" RENAME TO "UserPreferences_iamUserId_key";

View File

@@ -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[]

View File

@@ -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);

View File

@@ -25,9 +25,6 @@ export class FraudService {
context: Record<string, unknown>,
): 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<void> {
async blockUserTemporarily(iamUserId: string, durationMinutes: number): Promise<void> {
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<void> {
await this.prisma.user.update({
where: { id: userId },
async unblockUser(iamUserId: string): Promise<void> {
await this.prisma.passenger.updateMany({
where: { iamUserId },
data: { blockedUntil: null },
});
this.logger.log(`User ${userId} unblocked`);
this.logger.log(`Passenger (iamUserId=${iamUserId}) unblocked`);
}
/**

View File

@@ -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<string, unknown>,
): Promise<void> {
// 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<NotificationChannelType[]> {
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<string | null> {
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<string | null> {
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, ' ')

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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' }),

View File

@@ -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<FaydaConfig>('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],
);
}
}