import { Router, Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import bcrypt from 'bcryptjs'; import { UserRole, UserStatus } from '@prisma/client'; import { authService } from './auth.service'; import { loginSchema, registerSchema, refreshSchema } from './auth.schemas'; import { validate } from '../../middleware/validate'; import { authenticate } from '../../middleware/auth.middleware'; import { authRateLimit } from '../../middleware/rate-limit'; import { prisma } from '../../config/database'; import { verificationTokenService } from '../../services/verification-token.service'; import { passwordResetTokenService } from '../../services/password-reset-token.service'; import { emailService } from '../../services/email.service'; import { siteSettingsService } from '../settings/settings.service'; import { env } from '../../config/env'; import { logger } from '../../utils/logger'; import { createVerificationRateLimit, createResetRateLimit } from './auth.rate-limits'; const router = Router(); // POST /api/auth/login router.post( '/login', authRateLimit, validate(loginSchema), async (req: Request, res: Response, next: NextFunction) => { try { const result = await authService.login(req.body.email, req.body.password); res.json(result); } catch (err) { next(err); } } ); // POST /api/auth/register router.post( '/register', authRateLimit, validate(registerSchema), async (req: Request, res: Response, next: NextFunction) => { try { const result = await authService.register(req.body); res.status(201).json(result); } catch (err) { next(err); } } ); // POST /api/auth/verify-email const verifyEmailSchema = z.object({ token: z.string().min(1) }); router.post( '/verify-email', authRateLimit, validate(verifyEmailSchema), async (req: Request, res: Response, next: NextFunction) => { try { const { token } = req.body; const result = await verificationTokenService.verifyToken(token); if (!result.valid || !result.userId) { res.status(400).json({ error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' }, }); return; } const settings = await siteSettingsService.get(); const autoApprove = settings.autoApproveVerifiedUsers; const newStatus = autoApprove ? UserStatus.ACTIVE : UserStatus.PENDING_APPROVAL; await prisma.user.update({ where: { id: result.userId }, data: { emailVerified: true, status: newStatus }, }); // If not auto-approved, notify admins if (!autoApprove) { const user = await prisma.user.findUnique({ where: { id: result.userId } }); if (user) { const admins = await prisma.user.findMany({ where: { role: UserRole.SUPER_ADMIN, status: UserStatus.ACTIVE }, select: { email: true }, }); if (admins.length > 0) { await emailService.sendPendingApprovalNotification({ adminEmails: admins.map(a => a.email), newUserEmail: user.email, newUserName: user.name || '', }).catch(err => logger.error('Failed to send approval notification:', err)); } } } res.json({ verified: true, approved: autoApprove, message: autoApprove ? 'Email verified. You can now log in.' : 'Email verified. Your account is pending admin approval.', }); } catch (err) { next(err); } } ); // POST /api/auth/resend-verification const resendVerificationSchema = z.object({ email: z.string().email() }); router.post( '/resend-verification', createVerificationRateLimit(), validate(resendVerificationSchema), async (req: Request, res: Response, next: NextFunction) => { try { // Always return success to prevent user enumeration res.json({ message: 'If your email is registered and pending verification, a new verification link has been sent.' }); // Send asynchronously (don't block response) const user = await prisma.user.findUnique({ where: { email: req.body.email } }); if (user && user.status === UserStatus.PENDING_VERIFICATION) { const token = await verificationTokenService.createToken(user.id); const adminUrl = env.ADMIN_URL || 'http://localhost:3000'; const verificationUrl = `${adminUrl}/verify-email?token=${token}`; await emailService.sendVerificationEmail({ recipientEmail: user.email, recipientName: user.name || 'there', verificationUrl, }).catch(err => logger.error('Failed to resend verification email:', err)); } } catch (err) { next(err); } } ); // POST /api/auth/forgot-password const forgotPasswordSchema = z.object({ email: z.string().email() }); router.post( '/forgot-password', createResetRateLimit(), validate(forgotPasswordSchema), async (req: Request, res: Response, next: NextFunction) => { try { // Always return success to prevent user enumeration res.json({ message: 'If your email is registered, a password reset link has been sent.' }); // Send asynchronously const user = await prisma.user.findUnique({ where: { email: req.body.email } }); if (user && user.status === UserStatus.ACTIVE) { const token = await passwordResetTokenService.createToken(user.id); const adminUrl = env.ADMIN_URL || 'http://localhost:3000'; const resetUrl = `${adminUrl}/reset-password?token=${token}`; await emailService.sendPasswordResetEmail({ recipientEmail: user.email, recipientName: user.name || 'there', resetUrl, }).catch(err => logger.error('Failed to send password reset email:', err)); } } catch (err) { next(err); } } ); // POST /api/auth/reset-password const resetPasswordSchema = z.object({ token: z.string().min(1), password: z.string() .min(12, 'Password must be at least 12 characters') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[0-9]/, 'Password must contain at least one digit'), }); router.post( '/reset-password', authRateLimit, validate(resetPasswordSchema), async (req: Request, res: Response, next: NextFunction) => { try { const { token, password } = req.body; const result = await passwordResetTokenService.validateToken(token); if (!result.valid || !result.userId) { res.status(400).json({ error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' }, }); return; } const hashedPassword = await bcrypt.hash(password, 12); // Update password, mark token used, invalidate all refresh tokens await prisma.$transaction(async (tx) => { await tx.user.update({ where: { id: result.userId }, data: { password: hashedPassword }, }); await tx.refreshToken.deleteMany({ where: { userId: result.userId } }); }); await passwordResetTokenService.markTokenUsed(token); res.json({ message: 'Password has been reset. You can now log in with your new password.' }); } catch (err) { next(err); } } ); // POST /api/auth/refresh router.post( '/refresh', authRateLimit, validate(refreshSchema), async (req: Request, res: Response, next: NextFunction) => { try { const result = await authService.refreshTokens(req.body.refreshToken); res.json(result); } catch (err) { next(err); } } ); // POST /api/auth/logout router.post( '/logout', authRateLimit, validate(refreshSchema), async (req: Request, res: Response, next: NextFunction) => { try { await authService.logout(req.body.refreshToken); res.json({ message: 'Logged out' }); } catch (err) { next(err); } } ); // GET /api/auth/me router.get( '/me', authenticate, async (req: Request, res: Response, next: NextFunction) => { try { const user = await prisma.user.findUnique({ where: { id: req.user!.id }, select: { id: true, email: true, name: true, phone: true, role: true, roles: true, status: true, permissions: true, createdVia: true, emailVerified: true, lastLoginAt: true, createdAt: true, updatedAt: true, }, }); if (!user) { res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } }); return; } res.json(user); } catch (err) { next(err); } } ); export { router as authRouter };