import crypto from "node:crypto"; const COOKIE_NAME = "agentdock_session"; const MAX_AGE_MS = 12 * 60 * 60 * 1000; function parseCookies(header = "") { return Object.fromEntries(header.split(";").map((part) => { const index = part.indexOf("="); if (index < 0) return ["", ""]; return [part.slice(0, index).trim(), decodeURIComponent(part.slice(index + 1).trim())]; }).filter(([key]) => key)); } export class AuthService { constructor(password, { secureCookie = true } = {}) { this.salt = crypto.randomBytes(16); this.passwordHash = crypto.scryptSync(password, this.salt, 64); this.sessions = new Map(); this.attempts = new Map(); this.secureCookie = secureCookie; } verifyPassword(password) { const candidate = crypto.scryptSync(String(password || ""), this.salt, 64); return crypto.timingSafeEqual(candidate, this.passwordHash); } login(req, res, password) { const remote = req.ip || "unknown"; const attempt = this.attempts.get(remote) || { count: 0, resetAt: Date.now() + 60_000 }; if (Date.now() > attempt.resetAt) { attempt.count = 0; attempt.resetAt = Date.now() + 60_000; } if (attempt.count >= 8) return { ok: false, status: 429, error: "尝试次数过多,请一分钟后重试" }; if (!this.verifyPassword(password)) { attempt.count += 1; this.attempts.set(remote, attempt); return { ok: false, status: 401, error: "管理员密码不正确" }; } this.attempts.delete(remote); const token = crypto.randomBytes(32).toString("base64url"); this.sessions.set(token, Date.now() + MAX_AGE_MS); const secure = this.secureCookie ? "; Secure" : ""; res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; Path=/; HttpOnly${secure}; SameSite=Strict; Max-Age=${MAX_AGE_MS / 1000}`); return { ok: true }; } logout(req, res) { const token = parseCookies(req.headers.cookie)[COOKIE_NAME]; if (token) this.sessions.delete(token); const secure = this.secureCookie ? "; Secure" : ""; res.setHeader("Set-Cookie", `${COOKIE_NAME}=; Path=/; HttpOnly${secure}; SameSite=Strict; Max-Age=0`); } isAuthenticated(req) { const token = parseCookies(req.headers.cookie)[COOKIE_NAME]; if (!token) return false; const expiresAt = this.sessions.get(token); if (!expiresAt || expiresAt < Date.now()) { this.sessions.delete(token); return false; } return true; } middleware() { return (req, res, next) => { if (!this.isAuthenticated(req)) return res.status(401).json({ error: "Unauthorized" }); next(); }; } }