adamp b0e4e977c1 Initial commit: cookie-tracker
Girl Scout Cookie tracking app with Express/SQLite API and React/Vite client.

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

90 lines
2.1 KiB
JavaScript

const crypto = require('crypto');
const COOKIE_NAME = 'session';
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
function getSecret() {
const secret = process.env.APP_SECRET || process.env.APP_PASSWORD;
return secret || null;
}
function sign(payload) {
const secret = getSecret();
if (!secret) return null;
const data = JSON.stringify(payload);
const hmac = crypto.createHmac('sha256', secret).update(data).digest('hex');
const encoded = Buffer.from(data, 'utf8').toString('base64url');
return `${encoded}.${hmac}`;
}
function verify(token) {
const secret = getSecret();
if (!secret || !token) return false;
const parts = token.split('.');
if (parts.length !== 2) return false;
const [encoded, hmac] = parts;
let data;
try {
data = Buffer.from(encoded, 'base64url').toString('utf8');
} catch {
return false;
}
const expected = crypto.createHmac('sha256', secret).update(data).digest('hex');
if (expected !== hmac) return false;
let payload;
try {
payload = JSON.parse(data);
} catch {
return false;
}
if (payload.t && Date.now() - payload.t > MAX_AGE_MS) return false;
return true;
}
function createSessionCookie() {
const token = sign({ t: Date.now() });
if (!token) return null;
const opts = {
httpOnly: true,
sameSite: 'lax',
path: '/',
maxAge: Math.floor(MAX_AGE_MS / 1000),
};
if (process.env.NODE_ENV === 'production') opts.secure = true;
return { token, opts };
}
function clearSessionCookie() {
return {
httpOnly: true,
sameSite: 'lax',
path: '/',
maxAge: 0,
};
}
function authMiddleware(req, res, next) {
if (!process.env.APP_PASSWORD) {
return next();
}
const p = req.path || '';
if (p.startsWith('/auth')) return next();
const cookie = req.headers.cookie || '';
const match = cookie.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
const token = match ? match[1].trim() : null;
if (!token || !verify(token)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
module.exports = {
COOKIE_NAME,
getSecret,
sign,
verify,
createSessionCookie,
clearSessionCookie,
authMiddleware,
};