adamp a4ef21d099 Add security headers via helmet and improve rate limiting
Add helmet middleware for security headers (CSP, X-Content-Type-Options,
X-Frame-Options, HSTS, Referrer-Policy) and disable X-Powered-By. Add a
global API rate limiter (100 req/min/IP) using express-rate-limit. Replace
the hand-rolled in-memory login rate limiter (~25 lines) with a dedicated
express-rate-limit instance (5 attempts/min/IP) on the login route.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 22:06:55 -06:00

66 lines
1.7 KiB
JavaScript

const express = require('express');
const crypto = require('crypto');
const rateLimit = require('express-rate-limit');
const router = express.Router();
const {
COOKIE_NAME,
verify,
createSessionCookie,
clearSessionCookie,
} = require('../middleware/auth');
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many login attempts. Try again later.' },
});
router.post('/login', loginLimiter, (req, res) => {
const password = process.env.APP_PASSWORD;
if (!password) {
return res.status(200).json({ ok: true });
}
const submitted = req.body?.password || '';
const submittedBuf = Buffer.from(String(submitted));
const passwordBuf = Buffer.from(password);
let match = false;
if (submittedBuf.length === passwordBuf.length) {
match = crypto.timingSafeEqual(submittedBuf, passwordBuf);
}
if (!match) {
return res.status(401).json({ error: 'Invalid password' });
}
const session = createSessionCookie();
if (!session) {
return res.status(500).json({ error: 'Auth not configured' });
}
const cookieOpts = { ...session.opts };
if (cookieOpts.secure && !req.secure) cookieOpts.secure = false;
res.cookie(COOKIE_NAME, session.token, cookieOpts);
res.json({ ok: true });
});
router.post('/logout', (req, res) => {
res.cookie(COOKIE_NAME, '', clearSessionCookie());
res.status(204).send();
});
router.get('/me', (req, res) => {
if (!process.env.APP_PASSWORD) {
return res.status(200).json({ ok: true });
}
const token = req.cookies?.[COOKIE_NAME] || null;
if (!token || !verify(token)) {
return res.status(401).json({ error: 'Unauthorized' });
}
res.json({ ok: true });
});
module.exports = router;