import { Injectable, UnauthorizedException, ConflictException, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '../../common/prisma.service'; import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; @Injectable() export class AuthService { constructor(private prisma: PrismaService, private jwt: JwtService) {} async register(dto: RegisterDto) { const exists = await this.prisma.user.findFirst({ where: { OR: [{ email: dto.email }, { phone: dto.phone }] }, }); if (exists) throw new ConflictException('Email or phone already registered'); const passwordHash = await bcrypt.hash(dto.password, 10); const user = await this.prisma.user.create({ data: { fullName: dto.fullName, email: dto.email, phone: dto.phone, passwordHash, nationality: dto.nationality, nationalId: dto.nationalId, passportNumber: dto.passportNumber }, }); const passenger = await this.prisma.passenger.create({ data: { userId: user.id } }); await this.prisma.loyaltyAccount.create({ data: { passengerId: passenger.id } }); await this.prisma.walletAccount.create({ data: { passengerId: passenger.id } }); await this.prisma.userPreferences.create({ data: { userId: user.id } }); await this.createAuditLog(user.id, 'USER_REGISTERED', 'User', user.id, null, { email: user.email }); return await this.signToken(user.id, user.email, user.role, passenger.id); } async login(dto: LoginDto) { const user = await this.prisma.user.findUnique({ where: { email: dto.email }, include: { passenger: true, agent: true }, }); if (!user) throw new UnauthorizedException('Invalid credentials'); if (user.lockedUntil && user.lockedUntil > new Date()) { throw new UnauthorizedException(`Account locked until ${user.lockedUntil.toISOString()}`); } if (!(await bcrypt.compare(dto.password, user.passwordHash))) { await this.prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: { increment: 1 }, lockedUntil: user.failedLoginAttempts >= 4 ? new Date(Date.now() + 15 * 60 * 1000) : null } }); throw new UnauthorizedException('Invalid credentials'); } await this.prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: 0, lockedUntil: null } }); await this.createAuditLog(user.id, 'USER_LOGIN', 'User', user.id, null, null); // Ensure passenger exists and get its ID let passengerId = user.passenger?.id; if (!passengerId) { // If passenger doesn't exist, create it const passenger = await this.prisma.passenger.create({ data: { userId: user.id } }); passengerId = passenger.id; // Also create loyalty and wallet accounts await this.prisma.loyaltyAccount.create({ data: { passengerId: passenger.id } }); await this.prisma.walletAccount.create({ data: { passengerId: passenger.id } }); } return await this.signToken(user.id, user.email, user.role, passengerId, user.agent?.id); } async requestOtp(dto: RequestOtpDto) { const code = Math.floor(100000 + Math.random() * 900000).toString(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); await this.prisma.otpCode.create({ data: { email: dto.email, code, purpose: dto.purpose, expiresAt } }); console.log(`[OTP] ${dto.email} - ${code} (${dto.purpose})`); return { sent: true, expiresIn: 600 }; } async verifyOtp(dto: VerifyOtpDto) { const otp = await this.prisma.otpCode.findFirst({ where: { email: dto.email, code: dto.code, purpose: dto.purpose, verified: false, expiresAt: { gt: new Date() } }, orderBy: { createdAt: 'desc' } }); if (!otp) throw new BadRequestException('Invalid or expired OTP'); await this.prisma.otpCode.update({ where: { id: otp.id }, data: { verified: true } }); return { verified: true }; } async requestPasswordReset(dto: RequestPasswordResetDto) { const user = await this.prisma.user.findUnique({ where: { email: dto.email } }); if (!user) return { sent: true }; const token = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 60 * 60 * 1000); await this.prisma.passwordResetToken.create({ data: { userId: user.id, token, expiresAt } }); console.log(`[PASSWORD_RESET] ${dto.email} - ${token}`); return { sent: true }; } async resetPassword(dto: ResetPasswordDto) { const resetToken = await this.prisma.passwordResetToken.findUnique({ where: { token: dto.token } }); if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) { throw new BadRequestException('Invalid or expired reset token'); } const passwordHash = await bcrypt.hash(dto.newPassword, 10); await this.prisma.user.update({ where: { id: resetToken.userId }, data: { passwordHash, failedLoginAttempts: 0, lockedUntil: null } }); await this.prisma.passwordResetToken.update({ where: { id: resetToken.id }, data: { used: true } }); await this.createAuditLog(resetToken.userId, 'PASSWORD_RESET', 'User', resetToken.userId, null, null); return { reset: true }; } private async signToken(userId: string, email: string, role: string, passengerId?: string, agentId?: string) { // Get the full user data to include fullName const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, fullName: true, role: true } }); const payload = { sub: userId, email, role, passengerId, agentId }; console.log('[AUTH] Creating JWT with payload:', payload); const token = this.jwt.sign(payload); console.log('[AUTH] JWT created, token length:', token.length); const response = { token, user: { id: userId, email, fullName: user?.fullName || email, role, passengerId, agentId } }; console.log('[AUTH] Returning user object with passengerId:', response.user.passengerId); return response; } private async createAuditLog(userId: string, action: string, entityType: string, entityId: string, oldData: any, newData: any) { await this.prisma.auditLog.create({ data: { userId, action, entityType, entityId, oldData, newData } }); } async getProfile(userId: string) { if (!userId) { throw new UnauthorizedException('User ID not found in token'); } const user = await this.prisma.user.findUnique({ where: { id: userId }, include: { passenger: { include: { loyalty: true, wallet: true, }, }, preferences: true, }, }); if (!user) throw new UnauthorizedException('User not found'); return { id: user.id, email: user.email, phone: user.phone, fullName: user.fullName, role: user.role, nationality: user.nationality, nationalityCode: user.nationalityCode, nationalId: user.nationalId, passportNumber: user.passportNumber, faydaVerified: user.faydaVerified, faydaVerifiedAt: user.faydaVerifiedAt, lastLoginAt: user.lastLoginAt, createdAt: user.createdAt, passenger: user.passenger ? { id: user.passenger.id, preferredLanguage: user.passenger.preferredLanguage, loyalty: user.passenger.loyalty ? { tier: user.passenger.loyalty.tier, pointsBalance: user.passenger.loyalty.pointsBalance, lifetimePoints: user.passenger.loyalty.lifetimePoints, } : null, wallet: user.passenger.wallet ? { balanceMinor: user.passenger.wallet.balanceMinor, currency: user.passenger.wallet.currency, } : null, } : null, preferences: user.preferences, }; } async logout(userId: string) { // Invalidate all active sessions for this user await this.prisma.session.deleteMany({ where: { userId } }); // Log the logout action await this.createAuditLog(userId, 'USER_LOGOUT', 'User', userId, null, null); return { success: true, message: 'Logged out successfully' }; } }