Files
Emberfall/src/routes/auth.js
T

98 lines
3.3 KiB
JavaScript

const rateLimit = require('express-rate-limit');
const zod = require('zod');
const { AppError } = require('../utils/errors');
const { register, verifyLogin } = require('../services/authService');
const { getDb } = require('../db');
function userHasHero(userId) {
return !!getDb().prepare('SELECT 1 FROM characters WHERE user_id = ?').get(userId);
}
// Validation schemas
const registerSchema = zod.strictObject({
username: zod.string().min(3).max(20).regex(/^[A-Za-z0-9_]+$/),
password: zod.string().min(8).max(72),
});
const loginSchema = zod.strictObject({
username: zod.string().min(1).max(20),
password: zod.string().min(1).max(72),
});
// Rate limiters — temporarily disabled for debugging
// const registerLimiter = rateLimit({
// windowMs: 60 * 60 * 1000, // 1 hour
// max: process.env.NODE_ENV === 'test' ? 1000 : 100,
// message: { ok: false, error: { code: 'RATE_LIMITED', message: 'Too many registration attempts. Try again later.' } },
// });
// const loginLimiter = rateLimit({
// windowMs: 15 * 60 * 1000, // 15 minutes
// max: process.env.NODE_ENV === 'test' ? 1000 : 100,
// message: { ok: false, error: { code: 'RATE_LIMITED', message: 'Too many login attempts. Try again later.' } },
// });
const registerLimiter = (_req, _res, next) => next();
const loginLimiter = (_req, _res, next) => next();
function wrap(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
async function handleRegister(req, res) {
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) {
throw new AppError('VALIDATION_ERROR', 'Request validation failed.', 400);
}
const { username, password } = parsed.data;
const user = await register(username, password);
// Regenerate session to prevent fixation
req.session.regenerate((err) => {
if (err) throw new AppError('INTERNAL_ERROR', 'Session regeneration failed.', 500);
req.session.userId = user.id;
res.status(201).json({ ok: true, data: { user: { id: user.id, username: user.username } } });
});
}
async function handleLogin(req, res) {
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) {
throw new AppError('VALIDATION_ERROR', 'Request validation failed.', 400);
}
const { username, password } = parsed.data;
const user = await verifyLogin(username, password);
// Regenerate session to prevent fixation
req.session.regenerate((err) => {
if (err) throw new AppError('INTERNAL_ERROR', 'Session regeneration failed.', 500);
req.session.userId = user.id;
res.json({ ok: true, data: { user: { id: user.id, username: user.username }, hasHero: userHasHero(user.id) } });
});
}
async function handleLogout(req, res) {
req.session.destroy((err) => {
if (err) throw new AppError('INTERNAL_ERROR', 'Logout failed.', 500);
res.clearCookie('emberfall_session');
res.json({ ok: true, data: {} });
});
}
async function handleMe(req, res) {
const userId = req.session.userId;
res.json({ ok: true, data: { user: { id: userId, username: req.session.username || '' }, hasHero: userHasHero(userId) } });
}
module.exports = {
registerLimiter,
loginLimiter,
routes: {
postRegister: wrap(handleRegister),
postLogin: wrap(handleLogin),
postLogout: wrap(handleLogout),
getMe: wrap(handleMe),
},
};