Girl Scout Cookie tracking app with Express/SQLite API and React/Vite client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
2.1 KiB
JavaScript
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,
|
|
};
|