285 lines
8.7 KiB
TypeScript

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 };