mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
feat( iam ): integrate IAM for auth — register, login, lazy provisioning
This commit is contained in:
@@ -47,6 +47,7 @@ import { FraudModule } from './modules/fraud/fraud.module';
|
||||
import { SeatClassesModule } from './modules/seat-classes/seat-classes.module';
|
||||
import { FareEngineModule } from './modules/fare-engine/fare-engine.module';
|
||||
import { VerifaydaModule } from './modules/verifayda/verifayda.module';
|
||||
import {AuthModule} from "@/modules/auth/auth.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -74,6 +75,7 @@ import { VerifaydaModule } from './modules/verifayda/verifayda.module';
|
||||
TriaIamModule.forRoot(),
|
||||
PrismaModule,
|
||||
I18nModule,
|
||||
AuthModule,
|
||||
StationsModule,
|
||||
FleetModule,
|
||||
SchedulesModule,
|
||||
@@ -107,7 +109,11 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
) { }
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
await this.seeder.run();
|
||||
try {
|
||||
await this.seeder.run();
|
||||
} catch (err) {
|
||||
console.error('[DataSeeder] Seed failed (non-fatal during IAM migration phase):', (err as Error).message);
|
||||
}
|
||||
// await this.edrOrgSeeder.run();
|
||||
// await this.demoUsersSeeder.run();
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ export class SessionActivityInterceptor implements NestInterceptor {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const user = request.user;
|
||||
|
||||
if (user?.userId) {
|
||||
if (user?.id) {
|
||||
const session = await this.prisma.session.findFirst({
|
||||
where: { userId: user.userId },
|
||||
where: { userId: user.id },
|
||||
orderBy: { lastActivityAt: 'desc' },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,82 +1,62 @@
|
||||
import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PassengerAuthService } from './passenger-auth.service';
|
||||
import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private service: AuthService) {}
|
||||
constructor(
|
||||
private service: AuthService,
|
||||
private passengerAuthService: PassengerAuthService,
|
||||
) {}
|
||||
|
||||
@Post('register')
|
||||
@ApiOperation({
|
||||
summary: 'Register new passenger account',
|
||||
description: 'Create a new passenger account with email, phone, and password. Returns user details and JWT token for immediate login.'
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Account created successfully. Returns user object and JWT token.' })
|
||||
@ApiResponse({ status: 400, description: 'Validation error (invalid email, weak password, etc.)' })
|
||||
@ApiOperation({ summary: 'Register new passenger account' })
|
||||
@ApiResponse({ status: 201, description: 'Account created. Returns token + user.' })
|
||||
@ApiResponse({ status: 409, description: 'Email or phone already registered' })
|
||||
@ApiBody({ type: RegisterDto })
|
||||
register(@Body() dto: RegisterDto) { return this.service.register(dto); }
|
||||
register(@Request() req: any, @Body() dto: RegisterDto) {
|
||||
return this.passengerAuthService.register(dto, req);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Login with email and password',
|
||||
description: 'Authenticate user and receive JWT token. Token expires in 7 days by default. Failed login attempts are tracked and account may be locked after 5 consecutive failures.'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Login successful. Returns JWT token and user details.' })
|
||||
@ApiResponse({ status: 401, description: 'Invalid credentials or account locked' })
|
||||
@ApiResponse({ status: 403, description: 'Account temporarily blocked due to fraud detection' })
|
||||
@ApiOperation({ summary: 'Login with email and password' })
|
||||
@ApiResponse({ status: 200, description: 'Login successful. Returns token + passengerId.' })
|
||||
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
||||
@ApiBody({ type: LoginDto })
|
||||
login(@Body() dto: LoginDto) { return this.service.login(dto); }
|
||||
login(@Request() req: any, @Body() dto: LoginDto) {
|
||||
return this.passengerAuthService.login(dto, req);
|
||||
}
|
||||
|
||||
@Post('otp/request')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Request OTP verification code',
|
||||
description: 'Send a 6-digit OTP code to user email. Code expires in 10 minutes. Used for registration verification, password reset, or two-factor authentication.'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'OTP sent successfully to email' })
|
||||
@ApiResponse({ status: 404, description: 'Email not found (for PASSWORD_RESET purpose)' })
|
||||
@ApiResponse({ status: 429, description: 'Too many OTP requests. Please wait before requesting again.' })
|
||||
@ApiOperation({ summary: 'Request OTP verification code' })
|
||||
@ApiResponse({ status: 200, description: 'OTP sent successfully' })
|
||||
@ApiBody({ type: RequestOtpDto })
|
||||
requestOtp(@Body() dto: RequestOtpDto) { return this.service.requestOtp(dto); }
|
||||
|
||||
@Post('otp/verify')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Verify OTP code',
|
||||
description: 'Validate the 6-digit OTP code sent to user email. Code must match and not be expired.'
|
||||
})
|
||||
@ApiOperation({ summary: 'Verify OTP code' })
|
||||
@ApiResponse({ status: 200, description: 'OTP verified successfully' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid or expired OTP code' })
|
||||
@ApiResponse({ status: 404, description: 'No OTP found for this email and purpose' })
|
||||
@ApiBody({ type: VerifyOtpDto })
|
||||
verifyOtp(@Body() dto: VerifyOtpDto) { return this.service.verifyOtp(dto); }
|
||||
|
||||
@Post('password/reset-request')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Request password reset link',
|
||||
description: 'Send password reset link to user email. Link contains a secure token valid for 1 hour.'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Password reset email sent successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Email not found' })
|
||||
@ApiResponse({ status: 429, description: 'Too many reset requests. Please wait before trying again.' })
|
||||
@ApiOperation({ summary: 'Request password reset link' })
|
||||
@ApiResponse({ status: 200, description: 'Password reset email sent' })
|
||||
@ApiBody({ type: RequestPasswordResetDto })
|
||||
requestPasswordReset(@Body() dto: RequestPasswordResetDto) { return this.service.requestPasswordReset(dto); }
|
||||
|
||||
@Post('password/reset')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Reset password with token',
|
||||
description: 'Reset user password using the token received via email. Token is single-use and expires after 1 hour.'
|
||||
})
|
||||
@ApiOperation({ summary: 'Reset password with token' })
|
||||
@ApiResponse({ status: 200, description: 'Password reset successfully' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid, expired, or already used token' })
|
||||
@ApiResponse({ status: 404, description: 'User not found' })
|
||||
@ApiBody({ type: ResetPasswordDto })
|
||||
resetPassword(@Body() dto: ResetPasswordDto) { return this.service.resetPassword(dto); }
|
||||
|
||||
@@ -84,137 +64,34 @@ export class AuthController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Logout current user',
|
||||
description: `Logout the authenticated user and invalidate their session.
|
||||
@ApiOperation({ summary: 'Logout current user' })
|
||||
@ApiResponse({ status: 200, description: 'Logout successful' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
logout(@Request() req: any) {
|
||||
const userId = req.user?.id ?? req.user?.userId;
|
||||
if (!userId) throw new UnauthorizedException('User not authenticated');
|
||||
return this.service.logout(userId);
|
||||
}
|
||||
|
||||
### What happens:
|
||||
- Invalidates the current session token
|
||||
- Records logout in audit log
|
||||
- Frontend should clear stored token and redirect to home
|
||||
|
||||
### Authentication:
|
||||
- **Required**: JWT Bearer Token
|
||||
- Token will be invalidated after successful logout`
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Logout successful',
|
||||
schema: {
|
||||
example: {
|
||||
success: true,
|
||||
message: 'Logged out successfully'
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' })
|
||||
logout(@Request() req: any) {
|
||||
if (!req.user || !req.user.userId) {
|
||||
throw new UnauthorizedException('User not authenticated');
|
||||
}
|
||||
return this.service.logout(req.user.userId);
|
||||
@Get('me')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: '[DEV] Inspect raw JWT payload — shows full req.user from JwtGuard' })
|
||||
@ApiResponse({ status: 200, description: 'Returns the full req.user object set by JwtGuard' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
getMe(@Request() req: any) {
|
||||
return { user: req.user };
|
||||
}
|
||||
|
||||
@Get('profile')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Get current user profile',
|
||||
description: `**Returns complete user profile with all connected data**
|
||||
|
||||
---
|
||||
|
||||
### Response Includes
|
||||
|
||||
#### User Information
|
||||
- Basic details (id, email, phone, fullName, role)
|
||||
- Nationality and document information
|
||||
- Fayda verification status
|
||||
- Account timestamps (created, last login)
|
||||
|
||||
#### Passenger Data (if role=PASSENGER)
|
||||
- Passenger ID and preferences
|
||||
- **Loyalty Account**: Tier, points balance, lifetime points
|
||||
- **Wallet Account**: Balance (minor units), currency
|
||||
|
||||
#### User Preferences
|
||||
- Language, notification settings, etc.
|
||||
|
||||
---
|
||||
|
||||
### Use Cases
|
||||
|
||||
1. **App Initialization**: Fetch on app load to get user context
|
||||
|
||||
2. **Profile Pre-fill**: Use data to auto-fill booking forms
|
||||
|
||||
3. **Verification Check**: Check \`faydaVerified\` before registration
|
||||
|
||||
4. **Loyalty Display**: Show tier and points in UI
|
||||
|
||||
5. **Wallet Balance**: Display available balance
|
||||
|
||||
---
|
||||
|
||||
### Authentication
|
||||
- **Required**: JWT Bearer Token
|
||||
- Token must be valid and not expired
|
||||
- Returns profile for authenticated user only`,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'User profile retrieved successfully',
|
||||
schema: {
|
||||
example: {
|
||||
id: 'user-uuid-123',
|
||||
email: 'kelemu@email.com',
|
||||
phone: '+251911234567',
|
||||
fullName: 'Kelemu Abebe',
|
||||
role: 'PASSENGER',
|
||||
nationality: 'Ethiopian',
|
||||
nationalityCode: 'ET',
|
||||
nationalId: null,
|
||||
passportNumber: null,
|
||||
faydaVerified: true,
|
||||
faydaVerifiedAt: '2024-01-15T10:30:00.000Z',
|
||||
lastLoginAt: '2024-01-20T14:22:00.000Z',
|
||||
createdAt: '2023-12-01T08:00:00.000Z',
|
||||
passenger: {
|
||||
id: 'passenger-uuid-456',
|
||||
preferredLanguage: 'am',
|
||||
loyalty: {
|
||||
tier: 'SILVER',
|
||||
pointsBalance: 1500,
|
||||
lifetimePoints: 3000
|
||||
},
|
||||
wallet: {
|
||||
balanceMinor: 50000,
|
||||
currency: 'ETB'
|
||||
}
|
||||
},
|
||||
preferences: {
|
||||
emailNotifications: true,
|
||||
smsNotifications: true,
|
||||
language: 'am'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized - Invalid or missing JWT token',
|
||||
schema: {
|
||||
example: {
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized'
|
||||
}
|
||||
}
|
||||
})
|
||||
getProfile(@Request() req: any) {
|
||||
console.log('Profile request - User from JWT:', req.user);
|
||||
if (!req.user || !req.user.userId) {
|
||||
throw new UnauthorizedException('User not authenticated');
|
||||
}
|
||||
return this.service.getProfile(req.user.userId);
|
||||
@ApiOperation({ summary: 'Get current user profile' })
|
||||
@ApiResponse({ status: 200, description: 'User profile retrieved successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
getProfile(@Request() req: any) {
|
||||
const userId = req.user?.id ?? req.user?.userId;
|
||||
if (!userId) throw new UnauthorizedException('User not authenticated');
|
||||
return this.service.getProfile(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,20 +27,29 @@ export class RegisterDto {
|
||||
@IsString()
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Password (minimum 8 characters)',
|
||||
@ApiProperty({
|
||||
description: 'Password (minimum 8 characters)',
|
||||
example: 'SecurePass123',
|
||||
minLength: 8,
|
||||
format: 'password'
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Nationality of the passenger',
|
||||
example: 'Ethiopian'
|
||||
})
|
||||
@ApiPropertyOptional({
|
||||
description: 'Confirm password (must match password)',
|
||||
example: 'SecurePass123',
|
||||
format: 'password'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
confirmPassword?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Nationality of the passenger',
|
||||
example: 'Ethiopian'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nationality?: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PassengerAuthService } from './passenger-auth.service';
|
||||
import { JwtStrategy } from '../../common/jwt.strategy';
|
||||
|
||||
@Module({
|
||||
@@ -18,7 +19,7 @@ import { JwtStrategy } from '../../common/jwt.strategy';
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
providers: [AuthService, PassengerAuthService, JwtStrategy],
|
||||
exports: [JwtModule],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -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