Files
edr-platform/apps/edr-passenger-api/src/modules/auth/passenger-auth.service.ts

259 lines
8.9 KiB
TypeScript

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<IamAuthService> {
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<IamUserRow[]>(
`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<IamUserRow[]>(
`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<void> {
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);
}
}
}