mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 13:10:56 +00:00
refactor( iam ): remove prisma.user references from passenger-side services
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Passenger" ADD COLUMN "blockedUntil" TIMESTAMP(3);
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "UserPreferences_userId_key" RENAME TO "UserPreferences_iamUserId_key";
|
||||
@@ -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[]
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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, ' ')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user