"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const asyncHandler_1 = require("../utils/asyncHandler"); const db_1 = require("../db"); const schema_1 = require("../db/schema"); const drizzle_orm_1 = require("drizzle-orm"); const auth_1 = require("../utils/auth"); const errorHandler_1 = require("../middleware/errorHandler"); const zod_1 = require("zod"); const bcryptjs_1 = __importDefault(require("bcryptjs")); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const crypto_1 = __importDefault(require("crypto")); const router = (0, express_1.Router)(); // Rate limiting for auth endpoints const authLimiter = (0, express_rate_limit_1.default)({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // limit each IP to 10 requests per windowMs message: { error: { code: 'RATE_LIMITED', message: 'Too many attempts. Please try again later.', }, }, standardHeaders: true, legacyHeaders: false, }); const loginLimiter = (0, express_rate_limit_1.default)({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // stricter limit for login message: { error: { code: 'RATE_LIMITED', message: 'Too many login attempts. Please try again later.', }, }, standardHeaders: true, legacyHeaders: false, }); const passwordResetLimiter = (0, express_rate_limit_1.default)({ windowMs: 60 * 60 * 1000, // 1 hour max: 3, // very strict for password reset message: { error: { code: 'RATE_LIMITED', message: 'Too many password reset requests. Please try again later.', }, }, standardHeaders: true, legacyHeaders: false, }); const usernameSchema = zod_1.z .string() .min(3) .max(30) .regex(/^[a-zA-Z0-9_.-]+$/, 'Username can only contain letters, numbers, dots, dashes and underscores'); const registerSchema = zod_1.z.object({ username: usernameSchema, password: zod_1.z.string().min(8), }); const loginSchema = zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string(), }); const forgotPasswordSchema = zod_1.z.object({ username: zod_1.z.string().min(1), }); const resetPasswordSchema = zod_1.z.object({ token: zod_1.z.string().min(1), password: zod_1.z.string().min(8), }); const BCRYPT_ROUNDS = 12; async function hashPassword(password) { return bcryptjs_1.default.hash(password, BCRYPT_ROUNDS); } async function verifyPassword(password, hash) { return bcryptjs_1.default.compare(password, hash); } function generateResetToken() { return crypto_1.default.randomBytes(32).toString('hex'); } router.post('/register', authLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => { const data = registerSchema.parse(req.body); const existing = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1); if (existing.length > 0) { throw new errorHandler_1.AppError('USERNAME_EXISTS', 'Username already registered', 409); } const userId = (0, auth_1.generateId)('user'); const now = (0, auth_1.getCurrentTimestamp)(); await db_1.db.insert(schema_1.users).values({ id: userId, username: data.username, passwordHash: await hashPassword(data.password), createdAt: now, }); const token = (0, auth_1.generateToken)({ userId, username: data.username }); res.status(201).json({ user: { id: userId, username: data.username }, token, }); })); router.post('/login', loginLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => { const data = loginSchema.parse(req.body); const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1); if (user.length === 0 || !verifyPassword(data.password, user[0].passwordHash)) { throw new errorHandler_1.AppError('INVALID_CREDENTIALS', 'Invalid username or password', 401); } const token = (0, auth_1.generateToken)({ userId: user[0].id, username: user[0].username }); res.json({ user: { id: user[0].id, username: user[0].username }, token, }); })); router.get('/me', (0, asyncHandler_1.asyncHandler)(async (req, res) => { if (!req.user) { throw new errorHandler_1.AppError('UNAUTHORIZED', 'Authentication required', 401); } const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.id, req.user.userId)).limit(1); if (user.length === 0) { throw new errorHandler_1.AppError('NOT_FOUND', 'User not found', 404); } res.json({ id: user[0].id, username: user[0].username }); })); router.post('/forgot-password', passwordResetLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => { const data = forgotPasswordSchema.parse(req.body); const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1); // Always return success to prevent username enumeration if (user.length === 0) { res.json({ success: true }); return; } const resetToken = generateResetToken(); const resetTokenExpiry = Date.now() + 3600000; // 1 hour await db_1.db .update(schema_1.users) .set({ resetToken, resetTokenExpiry }) .where((0, drizzle_orm_1.eq)(schema_1.users.id, user[0].id)); // In production, send email with reset link // For now, return token in response (dev only) if (process.env.NODE_ENV !== 'production') { res.json({ success: true, resetToken }); } else { res.json({ success: true }); } })); router.post('/reset-password', passwordResetLimiter, (0, asyncHandler_1.asyncHandler)(async (req, res) => { const data = resetPasswordSchema.parse(req.body); const user = await db_1.db .select() .from(schema_1.users) .where((0, drizzle_orm_1.eq)(schema_1.users.resetToken, data.token)) .limit(1); if (user.length === 0 || !user[0].resetTokenExpiry || user[0].resetTokenExpiry < Date.now()) { throw new errorHandler_1.AppError('INVALID_TOKEN', 'Invalid or expired reset token', 400); } const newPasswordHash = await hashPassword(data.password); await db_1.db .update(schema_1.users) .set({ passwordHash: newPasswordHash, resetToken: null, resetTokenExpiry: null }) .where((0, drizzle_orm_1.eq)(schema_1.users.id, user[0].id)); res.json({ success: true }); })); exports.default = router; //# sourceMappingURL=auth.js.map