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>
66 lines
1.7 KiB
JavaScript
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;
|