mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 00:45:41 +00:00
feat( iam ): integrate IAM for auth — register, login, lazy provisioning
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
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 };
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user