import { Injectable, UnauthorizedException, ConflictException, BadRequestException, NotFoundException } 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, lastLoginAt: new Date() } }); 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 }; } async getUsers(filters: { search?: string; role?: string; status?: string; page?: number; pageSize?: number }) { const { search, role, status, page = 1, pageSize = 10 } = filters; const skip = (page - 1) * pageSize; const where: any = { role: { not: 'PASSENGER' }, // Exclude passenger accounts }; if (search) { where.OR = [ { email: { contains: search, mode: 'insensitive' } }, { fullName: { contains: search, mode: 'insensitive' } }, ]; } if (role) { where.role = role; } // For status filtering, we check if user is active (no lock/block) or inactive if (status === 'ACTIVE') { where.AND = [ { blockedUntil: { lte: new Date() } }, { lockedUntil: { lte: new Date() } } ]; } else if (status === 'INACTIVE') { where.OR = [ { blockedUntil: { gt: new Date() } }, { lockedUntil: { gt: new Date() } } ]; } const [items, total] = await Promise.all([ this.prisma.user.findMany({ where, select: { id: true, email: true, fullName: true, role: true, lastLoginAt: true, createdAt: true, blockedUntil: true, lockedUntil: true, }, skip, take: pageSize, orderBy: { createdAt: 'desc' }, }), this.prisma.user.count({ where }), ]); return { items: items.map(user => ({ id: user.id, email: user.email, fullName: user.fullName, role: user.role, lastLogin: user.lastLoginAt, status: (!user.blockedUntil || user.blockedUntil <= new Date()) && (!user.lockedUntil || user.lockedUntil <= new Date()) ? 'ACTIVE' : 'INACTIVE', })), total, page, pageSize, }; } async createUser(dto: { email: string; fullName: string; role: string; status?: string; password?: string }) { const exists = await this.prisma.user.findFirst({ where: { OR: [{ email: dto.email }] }, }); if (exists) throw new ConflictException('Email already registered'); const passwordHash = await bcrypt.hash(dto.password || 'TempPassword123!', 10); const user = await this.prisma.user.create({ data: { email: dto.email, fullName: dto.fullName, role: dto.role as any, phone: dto.email, // Use email as phone temporarily for unique constraint passwordHash, blockedUntil: dto.status === 'INACTIVE' ? new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) : undefined, }, select: { id: true, email: true, fullName: true, role: true, lastLoginAt: true, createdAt: true, }, }); await this.createAuditLog(user.id, 'USER_CREATED', 'User', user.id, null, { email: user.email, role: dto.role }); return user; } async updateUser(id: string, dto: Partial<{ email: string; fullName: string; role: string; status: string }>) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new NotFoundException('User not found'); const updateData: any = {}; if (dto.fullName) updateData.fullName = dto.fullName; if (dto.role) updateData.role = dto.role; if (dto.status === 'ACTIVE') { updateData.blockedUntil = null; updateData.lockedUntil = null; } else if (dto.status === 'INACTIVE') { updateData.blockedUntil = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); } const updated = await this.prisma.user.update({ where: { id }, data: updateData, select: { id: true, email: true, fullName: true, role: true, lastLoginAt: true, createdAt: true, }, }); await this.createAuditLog(id, 'USER_UPDATED', 'User', id, { oldData: user }, { newData: updateData }); return updated; } async deleteUser(id: string) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new NotFoundException('User not found'); // Don't actually delete, just deactivate await this.prisma.user.update({ where: { id }, data: { blockedUntil: new Date(), lockedUntil: new Date() }, }); await this.createAuditLog(id, 'USER_DELETED', 'User', id, { email: user.email }, null); return { deleted: true }; } async resetUserPassword(id: string, tempPassword: string) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new NotFoundException('User not found'); const passwordHash = await bcrypt.hash(tempPassword, 10); await this.prisma.user.update({ where: { id }, data: { passwordHash, failedLoginAttempts: 0, lockedUntil: null, }, }); await this.createAuditLog(id, 'PASSWORD_RESET_ADMIN', 'User', id, null, { resetBy: 'admin' }); return { reset: true, tempPassword }; } 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, devices: 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, devices: user.devices.map(device => ({ id: device.id, platform: device.platform, name: device.name, pushToken: device.pushToken, trusted: device.trusted, lastSeenAt: device.lastSeenAt, })), }; } 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' }; } }