const crypto = require('crypto'); const AUTH_COOKIE_NAME = 'accounting_session'; const AUTH_USERNAME = process.env.AUTH_USERNAME || 'admin'; const AUTH_PASSWORD = process.env.AUTH_PASSWORD || '1234'; const AUTH_SESSION_SECRET = process.env.AUTH_SESSION_SECRET || 'change-this-session-secret-before-public-deploy'; const AUTH_SESSION_MAX_AGE_SECONDS = 7 * 24 * 60 * 60; function safeCompare(left, right) { const leftBuffer = Buffer.from(String(left)); const rightBuffer = Buffer.from(String(right)); if (leftBuffer.length !== rightBuffer.length) { return false; } return crypto.timingSafeEqual(leftBuffer, rightBuffer); } function serializeCookie(name, value, options = {}) { const parts = [`${name}=${value}`]; if (options.maxAge !== undefined) { parts.push(`Max-Age=${options.maxAge}`); } parts.push(`Path=${options.path || '/'}`); parts.push(`SameSite=${options.sameSite || 'Lax'}`); if (options.httpOnly !== false) { parts.push('HttpOnly'); } if (options.secure) { parts.push('Secure'); } return parts.join('; '); } function parseCookies(req) { const cookieHeader = req.headers.cookie; if (!cookieHeader) { return {}; } return cookieHeader.split(';').reduce((cookies, item) => { const [rawName, ...rawValue] = item.trim().split('='); cookies[rawName] = rawValue.join('='); return cookies; }, {}); } function signPayload(payload) { return crypto.createHmac('sha256', AUTH_SESSION_SECRET).update(payload).digest('base64url'); } function createSessionToken(username) { const payload = Buffer.from( JSON.stringify({ username, expiresAt: Date.now() + AUTH_SESSION_MAX_AGE_SECONDS * 1000, }) ).toString('base64url'); return `${payload}.${signPayload(payload)}`; } function readSession(req) { const token = parseCookies(req)[AUTH_COOKIE_NAME]; if (!token) { return null; } const [payload, signature] = token.split('.'); if (!payload || !signature || !safeCompare(signature, signPayload(payload))) { return null; } try { const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); if (!decoded?.username || !Number.isFinite(decoded?.expiresAt)) { return null; } if (decoded.expiresAt < Date.now()) { return null; } if (!safeCompare(decoded.username, AUTH_USERNAME)) { return null; } return { username: AUTH_USERNAME }; } catch (error) { return null; } } function getCookieOptions(maxAge) { return { maxAge, httpOnly: true, path: '/', sameSite: 'Lax', secure: process.env.AUTH_COOKIE_SECURE === 'true', }; } function setAuthCookie(res, username) { res.setHeader( 'Set-Cookie', serializeCookie( AUTH_COOKIE_NAME, createSessionToken(username), getCookieOptions(AUTH_SESSION_MAX_AGE_SECONDS) ) ); } function clearAuthCookie(res) { res.setHeader('Set-Cookie', serializeCookie(AUTH_COOKIE_NAME, '', getCookieOptions(0))); } function isValidCredentials(username, password) { return ( safeCompare(String(username || '').trim(), AUTH_USERNAME) && safeCompare(String(password || ''), AUTH_PASSWORD) ); } function requireAuth(req, res, next) { const user = readSession(req); if (!user) { return res.status(401).json({ error: '请先登录后再继续访问' }); } req.authUser = user; next(); } module.exports = { AUTH_USERNAME, clearAuthCookie, isValidCredentials, readSession, requireAuth, setAuthCookie, };