mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
259 lines
8.9 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|