mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 09:28:19 +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
|
iamUserId String? @unique
|
||||||
defaultTravelerProfileId String?
|
defaultTravelerProfileId String?
|
||||||
preferredLanguage String?
|
preferredLanguage String?
|
||||||
|
blockedUntil DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
bookings Booking[]
|
bookings Booking[]
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ export class PassengerAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async register(dto: RegisterDto, req: any) {
|
async register(dto: RegisterDto, req: any) {
|
||||||
const existing = await this.prisma.user.findFirst({
|
const existing = await this.dataSource.query<{ id: string }[]>(
|
||||||
where: { OR: [{ email: dto.email }, { phone: dto.phone }] },
|
`SELECT id FROM iam.users WHERE email = $1 OR phone_number = $2 LIMIT 1`,
|
||||||
});
|
[dto.email, dto.phone],
|
||||||
if (existing) throw new ConflictException('Email or phone already registered');
|
);
|
||||||
|
if (existing.length) throw new ConflictException('Email or phone already registered');
|
||||||
|
|
||||||
const iamAuthService = await this.resolveIamAuthService(req);
|
const iamAuthService = await this.resolveIamAuthService(req);
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ export class FraudService {
|
|||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
): Promise<{ triggered: boolean; rules: string[] }> {
|
): Promise<{ triggered: boolean; rules: string[] }> {
|
||||||
const triggeredRules: 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)
|
// Check velocity rule (multiple bookings in short time)
|
||||||
if (eventType === 'booking.created') {
|
if (eventType === 'booking.created') {
|
||||||
@@ -157,24 +154,24 @@ export class FraudService {
|
|||||||
/**
|
/**
|
||||||
* Block user temporarily
|
* 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);
|
const blockedUntil = new Date(Date.now() + durationMinutes * 60 * 1000);
|
||||||
await this.prisma.user.update({
|
await this.prisma.passenger.updateMany({
|
||||||
where: { id: userId },
|
where: { iamUserId },
|
||||||
data: { blockedUntil },
|
data: { blockedUntil },
|
||||||
});
|
});
|
||||||
this.logger.warn(`User ${userId} blocked until ${blockedUntil.toISOString()}`);
|
this.logger.warn(`Passenger (iamUserId=${iamUserId}) blocked until ${blockedUntil.toISOString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unblock user
|
* Unblock user
|
||||||
*/
|
*/
|
||||||
async unblockUser(userId: string): Promise<void> {
|
async unblockUser(iamUserId: string): Promise<void> {
|
||||||
await this.prisma.user.update({
|
await this.prisma.passenger.updateMany({
|
||||||
where: { id: userId },
|
where: { iamUserId },
|
||||||
data: { blockedUntil: null },
|
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 { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto';
|
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto';
|
||||||
import { EmailAdapter, SmsAdapter, PushAdapter, NotificationChannel } from './notification.adapters';
|
import { EmailAdapter, SmsAdapter, PushAdapter, NotificationChannel } from './notification.adapters';
|
||||||
|
|
||||||
export type NotificationChannelType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP';
|
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()
|
@Injectable()
|
||||||
export class NotificationsService {
|
export class NotificationsService {
|
||||||
private readonly logger = new Logger(NotificationsService.name);
|
private readonly logger = new Logger(NotificationsService.name);
|
||||||
@@ -13,6 +17,7 @@ export class NotificationsService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
|
@InjectDataSource() private readonly dataSource: DataSource,
|
||||||
private emailAdapter: EmailAdapter,
|
private emailAdapter: EmailAdapter,
|
||||||
private smsAdapter: SmsAdapter,
|
private smsAdapter: SmsAdapter,
|
||||||
private pushAdapter: PushAdapter,
|
private pushAdapter: PushAdapter,
|
||||||
@@ -98,15 +103,17 @@ export class NotificationsService {
|
|||||||
|
|
||||||
const passenger = await this.prisma.passenger.findUnique({
|
const passenger = await this.prisma.passenger.findUnique({
|
||||||
where: { id: dto.passengerId },
|
where: { id: dto.passengerId },
|
||||||
include: { user: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (passenger?.user) {
|
if (passenger?.iamUserId) {
|
||||||
await this.emailAdapter.send(
|
const contact = await this.resolveContactInfo(passenger.iamUserId);
|
||||||
passenger.user.email,
|
if (contact.email) {
|
||||||
this.sanitize(dto.title),
|
await this.emailAdapter.send(
|
||||||
this.sanitize(dto.body),
|
contact.email,
|
||||||
);
|
this.sanitize(dto.title),
|
||||||
|
this.sanitize(dto.body),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
@@ -118,22 +125,20 @@ export class NotificationsService {
|
|||||||
body: string,
|
body: string,
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Try to find passenger by ID or email
|
|
||||||
let passengerId = recipient;
|
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)) {
|
if (!UUID_RE.test(recipient)) {
|
||||||
const user = await this.prisma.user.findFirst({
|
const iamUserId = await this.resolveIamUserId(recipient);
|
||||||
where: {
|
if (!iamUserId) {
|
||||||
OR: [{ email: recipient }, { phone: recipient }],
|
|
||||||
},
|
|
||||||
include: { passenger: true },
|
|
||||||
});
|
|
||||||
if (user?.passenger) {
|
|
||||||
passengerId = user.passenger.id;
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`Could not find passenger for recipient: ${recipient}`);
|
this.logger.warn(`Could not find passenger for recipient: ${recipient}`);
|
||||||
return;
|
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({
|
await this.prisma.notification.create({
|
||||||
@@ -165,13 +170,10 @@ export class NotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
|
private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const iamUserId = await this.resolveIamUserId(recipient);
|
||||||
where: { OR: [{ id: recipient }, { email: recipient }, { phone: recipient }] },
|
const preferences = iamUserId
|
||||||
include: { passenger: { select: { iamUserId: true } } },
|
? await this.prisma.userPreferences.findUnique({ where: { iamUserId } })
|
||||||
});
|
: null;
|
||||||
|
|
||||||
const iamUserId = user?.passenger?.iamUserId ?? recipient;
|
|
||||||
const preferences = await this.prisma.userPreferences.findUnique({ where: { iamUserId } });
|
|
||||||
|
|
||||||
if (!preferences) {
|
if (!preferences) {
|
||||||
return ['IN_APP', 'EMAIL'];
|
return ['IN_APP', 'EMAIL'];
|
||||||
@@ -189,27 +191,38 @@ export class NotificationsService {
|
|||||||
recipient: string,
|
recipient: string,
|
||||||
channel: NotificationChannelType,
|
channel: NotificationChannelType,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const iamUserId = await this.resolveIamUserId(recipient);
|
||||||
where: {
|
if (!iamUserId) return null;
|
||||||
OR: [{ id: recipient }, { email: recipient }, { phone: recipient }],
|
const contact = await this.resolveContactInfo(iamUserId);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
switch (channel) {
|
switch (channel) {
|
||||||
case 'EMAIL':
|
case 'EMAIL': return contact.email;
|
||||||
return user.email;
|
case 'SMS': return contact.phone;
|
||||||
case 'SMS':
|
case 'PUSH': return iamUserId;
|
||||||
return user.phone;
|
default: return null;
|
||||||
case 'PUSH':
|
|
||||||
// Would need to fetch device push token
|
|
||||||
return user.id;
|
|
||||||
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 {
|
private sanitize(value: string): string {
|
||||||
return value
|
return value
|
||||||
.replace(/[\r\n]/g, ' ')
|
.replace(/[\r\n]/g, ' ')
|
||||||
|
|||||||
@@ -52,25 +52,17 @@ export class PassengersController {
|
|||||||
})
|
})
|
||||||
@ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' })
|
@ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' })
|
||||||
async getMe(@Request() req: any) {
|
async getMe(@Request() req: any) {
|
||||||
if (!req.user || !req.user.userId) {
|
if (!req.user || !req.user.id) {
|
||||||
throw new UnauthorizedException('User not authenticated');
|
throw new UnauthorizedException('User not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.prisma.user.findUnique({
|
const passenger = await this.prisma.passenger.findUnique({
|
||||||
where: { id: req.user.userId },
|
where: { iamUserId: req.user.id },
|
||||||
include: {
|
|
||||||
passenger: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
if (!passenger) return null;
|
||||||
if (!user || !user.passenger) {
|
return this.service.getProfile(passenger.id);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.service.getProfile(user.passenger.id);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If profile lookup fails for any reason, return null to allow app to continue
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ export class PassengersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProfile(passengerId: string) {
|
async getProfile(passengerId: string) {
|
||||||
|
|
||||||
|
console.log("here");
|
||||||
|
|
||||||
const p = await this.prisma.passenger.findUnique({
|
const p = await this.prisma.passenger.findUnique({
|
||||||
where: { id: passengerId },
|
where: { id: passengerId },
|
||||||
include: {
|
include: {
|
||||||
@@ -230,36 +233,18 @@ export class PassengersService {
|
|||||||
email: dto.email,
|
email: dto.email,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If logged in, update user profile and link passenger
|
// If logged in, link passenger
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
// dto.userId is the IAM user UUID — resolve via Passenger.iamUserId
|
|
||||||
const linkedPassenger = await this.prisma.passenger.findUnique({
|
const linkedPassenger = await this.prisma.passenger.findUnique({
|
||||||
where: { iamUserId: dto.userId },
|
where: { iamUserId: dto.userId },
|
||||||
include: { user: true },
|
|
||||||
});
|
});
|
||||||
const user = linkedPassenger?.user ?? null;
|
|
||||||
|
|
||||||
if (!user) {
|
if (!linkedPassenger) {
|
||||||
throw new BadRequestException('User not found');
|
throw new BadRequestException('Passenger 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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: linkedPassenger?.id || user.id,
|
id: linkedPassenger.id,
|
||||||
passengerName: finalData.passengerName,
|
passengerName: finalData.passengerName,
|
||||||
dateOfBirth: finalData.dateOfBirth,
|
dateOfBirth: finalData.dateOfBirth,
|
||||||
nationality: finalData.nationality,
|
nationality: finalData.nationality,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
service = new VerifaydaService(
|
service = new VerifaydaService(
|
||||||
buildConfigService(cfg),
|
buildConfigService(cfg),
|
||||||
prisma as unknown as PrismaService,
|
prisma as unknown as PrismaService,
|
||||||
|
{ query: jest.fn().mockResolvedValue([]) } as any,
|
||||||
);
|
);
|
||||||
(global as any).fetch = jest.fn();
|
(global as any).fetch = jest.fn();
|
||||||
});
|
});
|
||||||
@@ -126,6 +127,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
|||||||
const disabledService = new VerifaydaService(
|
const disabledService = new VerifaydaService(
|
||||||
buildConfigService(buildConfig({ enabled: false })),
|
buildConfigService(buildConfig({ enabled: false })),
|
||||||
prisma as unknown as PrismaService,
|
prisma as unknown as PrismaService,
|
||||||
|
{ query: jest.fn().mockResolvedValue([]) } as any,
|
||||||
);
|
);
|
||||||
await expect(
|
await expect(
|
||||||
disabledService.startVerification({ purpose: 'PURCHASE' }),
|
disabledService.startVerification({ purpose: 'PURCHASE' }),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config';
|
import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config';
|
||||||
@@ -85,6 +87,7 @@ export class VerifaydaService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
|
@InjectDataSource() private readonly dataSource: DataSource,
|
||||||
) {
|
) {
|
||||||
const fayda = this.config.get<FaydaConfig>('fayda');
|
const fayda = this.config.get<FaydaConfig>('fayda');
|
||||||
if (!fayda) {
|
if (!fayda) {
|
||||||
@@ -438,23 +441,16 @@ export class VerifaydaService {
|
|||||||
|
|
||||||
const iamUserId = session.iamUserId;
|
const iamUserId = session.iamUserId;
|
||||||
if (iamUserId && session.saveToAccount) {
|
if (iamUserId && session.saveToAccount) {
|
||||||
const passenger = await this.prisma.passenger.findUnique({
|
const conflicts = await this.dataSource.query<{ id: string }[]>(
|
||||||
where: { iamUserId },
|
`SELECT id FROM iam.users WHERE metadata->>'faydaSub' = $1 AND id != $2 LIMIT 1`,
|
||||||
select: { userId: true },
|
[normalized.sub, iamUserId],
|
||||||
});
|
);
|
||||||
const localUserId = passenger?.userId;
|
if (conflicts.length) throw new FaydaIdentityConflictException();
|
||||||
if (!localUserId) return;
|
|
||||||
|
|
||||||
const conflict = await this.prisma.user.findFirst({
|
await this.dataSource.query(
|
||||||
where: { faydaSub: normalized.sub, NOT: { id: localUserId } },
|
`UPDATE iam.users SET metadata = COALESCE(metadata, '{}') || $1::jsonb WHERE id = $2`,
|
||||||
select: { id: true },
|
[JSON.stringify({ faydaSub: normalized.sub, faydaVerified: true, faydaVerifiedAt: new Date().toISOString() }), iamUserId],
|
||||||
});
|
);
|
||||||
if (conflict) throw new FaydaIdentityConflictException();
|
|
||||||
|
|
||||||
await this.prisma.user.update({
|
|
||||||
where: { id: localUserId },
|
|
||||||
data: { faydaVerified: true, faydaVerifiedAt: new Date(), faydaSub: normalized.sub },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user