279 lines
12 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authRouter = void 0;
const express_1 = require("express");
const zod_1 = require("zod");
const bcryptjs_1 = __importDefault(require("bcryptjs"));
const client_1 = require("@prisma/client");
const auth_service_1 = require("./auth.service");
const auth_schemas_1 = require("./auth.schemas");
const validate_1 = require("../../middleware/validate");
const auth_middleware_1 = require("../../middleware/auth.middleware");
const rate_limit_1 = require("../../middleware/rate-limit");
const database_1 = require("../../config/database");
const verification_token_service_1 = require("../../services/verification-token.service");
const password_reset_token_service_1 = require("../../services/password-reset-token.service");
const email_service_1 = require("../../services/email.service");
const settings_service_1 = require("../settings/settings.service");
const env_1 = require("../../config/env");
const logger_1 = require("../../utils/logger");
const auth_rate_limits_1 = require("./auth.rate-limits");
const profile_service_1 = require("../people/profile.service");
const router = (0, express_1.Router)();
exports.authRouter = router;
// POST /api/auth/login
router.post('/login', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.loginSchema), async (req, res, next) => {
try {
const result = await auth_service_1.authService.login(req.body.email, req.body.password);
res.json(result);
}
catch (err) {
next(err);
}
});
// POST /api/auth/register
router.post('/register', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.registerSchema), async (req, res, next) => {
try {
const result = await auth_service_1.authService.register(req.body);
res.status(201).json(result);
}
catch (err) {
next(err);
}
});
// POST /api/auth/verify-email
const verifyEmailSchema = zod_1.z.object({ token: zod_1.z.string().min(1) });
router.post('/verify-email', rate_limit_1.authRateLimit, (0, validate_1.validate)(verifyEmailSchema), async (req, res, next) => {
try {
const { token } = req.body;
const result = await verification_token_service_1.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 settings_service_1.siteSettingsService.get();
const autoApprove = settings.autoApproveVerifiedUsers;
const newStatus = autoApprove ? client_1.UserStatus.ACTIVE : client_1.UserStatus.PENDING_APPROVAL;
await database_1.prisma.user.update({
where: { id: result.userId },
data: { emailVerified: true, status: newStatus },
});
// If not auto-approved, notify admins
if (!autoApprove) {
const user = await database_1.prisma.user.findUnique({ where: { id: result.userId } });
if (user) {
const admins = await database_1.prisma.user.findMany({
where: { role: client_1.UserRole.SUPER_ADMIN, status: client_1.UserStatus.ACTIVE },
select: { email: true },
});
if (admins.length > 0) {
await email_service_1.emailService.sendPendingApprovalNotification({
adminEmails: admins.map(a => a.email),
newUserEmail: user.email,
newUserName: user.name || '',
}).catch(err => logger_1.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 = zod_1.z.object({ email: zod_1.z.string().email() });
router.post('/resend-verification', (0, auth_rate_limits_1.createVerificationRateLimit)(), (0, validate_1.validate)(resendVerificationSchema), async (req, res, next) => {
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 database_1.prisma.user.findUnique({ where: { email: req.body.email } });
if (user && user.status === client_1.UserStatus.PENDING_VERIFICATION) {
const token = await verification_token_service_1.verificationTokenService.createToken(user.id);
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
const verificationUrl = `${adminUrl}/verify-email?token=${token}`;
await email_service_1.emailService.sendVerificationEmail({
recipientEmail: user.email,
recipientName: user.name || 'there',
verificationUrl,
}).catch(err => logger_1.logger.error('Failed to resend verification email:', err));
}
}
catch (err) {
next(err);
}
});
// POST /api/auth/forgot-password
const forgotPasswordSchema = zod_1.z.object({ email: zod_1.z.string().email() });
router.post('/forgot-password', (0, auth_rate_limits_1.createResetRateLimit)(), (0, validate_1.validate)(forgotPasswordSchema), async (req, res, next) => {
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 database_1.prisma.user.findUnique({ where: { email: req.body.email } });
if (user && user.status === client_1.UserStatus.ACTIVE) {
const token = await password_reset_token_service_1.passwordResetTokenService.createToken(user.id);
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
const resetUrl = `${adminUrl}/reset-password?token=${token}`;
await email_service_1.emailService.sendPasswordResetEmail({
recipientEmail: user.email,
recipientName: user.name || 'there',
resetUrl,
}).catch(err => logger_1.logger.error('Failed to send password reset email:', err));
}
}
catch (err) {
next(err);
}
});
// POST /api/auth/reset-password
const resetPasswordSchema = zod_1.z.object({
token: zod_1.z.string().min(1),
password: zod_1.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', rate_limit_1.authRateLimit, (0, validate_1.validate)(resetPasswordSchema), async (req, res, next) => {
try {
const { token, password } = req.body;
const result = await password_reset_token_service_1.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 bcryptjs_1.default.hash(password, 12);
// Update password, mark token used, invalidate all refresh tokens — all in one transaction
await database_1.prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: result.userId },
data: { password: hashedPassword },
});
await tx.refreshToken.deleteMany({ where: { userId: result.userId } });
await tx.passwordResetToken.update({
where: { token },
data: { usedAt: new Date() },
});
});
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', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.refreshSchema), async (req, res, next) => {
try {
const result = await auth_service_1.authService.refreshTokens(req.body.refreshToken);
res.json(result);
}
catch (err) {
next(err);
}
});
// POST /api/auth/logout
router.post('/logout', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.refreshSchema), async (req, res, next) => {
try {
await auth_service_1.authService.logout(req.body.refreshToken);
res.json({ message: 'Logged out' });
}
catch (err) {
next(err);
}
});
// GET /api/auth/me/profile-token
router.get('/me/profile-token', auth_middleware_1.authenticate, async (req, res, next) => {
try {
const userId = req.user.id;
// Look up existing Contact linked to this user
let contact = await database_1.prisma.contact.findUnique({ where: { userId } });
// Auto-create Contact if none exists
if (!contact) {
const user = await database_1.prisma.user.findUnique({
where: { id: userId },
select: { name: true, email: true },
});
if (!user) {
res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } });
return;
}
try {
contact = await database_1.prisma.contact.create({
data: {
displayName: user.name || user.email,
firstName: user.name?.split(' ')[0] || null,
lastName: user.name?.split(' ').slice(1).join(' ') || null,
email: user.email,
userId,
primarySource: 'USER',
},
});
}
catch (err) {
// Race condition: another request created the Contact — retry lookup
if (err.code === 'P2002') {
contact = await database_1.prisma.contact.findUnique({ where: { userId } });
}
if (!contact)
throw err;
}
}
// Generate profile token if Contact doesn't have one
if (!contact.profileToken) {
const result = await profile_service_1.profileService.generateProfileToken(contact.id);
res.json({ token: result.token });
return;
}
res.json({ token: contact.profileToken });
}
catch (err) {
next(err);
}
});
// GET /api/auth/me
router.get('/me', auth_middleware_1.authenticate, async (req, res, next) => {
try {
const user = await database_1.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);
}
});
//# sourceMappingURL=auth.routes.js.map