168 lines
6.7 KiB
JavaScript
168 lines
6.7 KiB
JavaScript
"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
|