"use strict"; /* ================= 认证:scrypt 密码哈希 + 服务端会话 ================= */ const crypto = require("node:crypto"); const SCRYPT_N = 16384, SCRYPT_R = 8, SCRYPT_P = 1, KEYLEN = 64; const SESSION_TTL = 7 * 24 * 3600 * 1000; // 7 天 const COOKIE_NAME = "sr_sid"; function hashPassword(pw) { const salt = crypto.randomBytes(16); const hash = crypto.scryptSync(pw, salt, KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }); return salt.toString("hex") + ":" + hash.toString("hex"); } function verifyPassword(pw, stored) { if (typeof stored !== "string") return false; const i = stored.indexOf(":"); if (i < 0) return false; const salt = Buffer.from(stored.slice(0, i), "hex"); const expect = Buffer.from(stored.slice(i + 1), "hex"); if (salt.length !== 16 || expect.length !== KEYLEN) return false; const got = crypto.scryptSync(pw, salt, KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }); return crypto.timingSafeEqual(got, expect); } function createSession(db, userId) { const token = crypto.randomBytes(32).toString("hex"); const now = Date.now(); db.prepare("INSERT INTO sessions(token,user_id,created_at,expires_at) VALUES(?,?,?,?)") .run(token, userId, now, now + SESSION_TTL); return token; } /* 查 session(含滑动续期:剩余不足一半时顺延) */ function getSessionUser(db, token) { if (!token || typeof token !== "string" || token.length > 128) return null; const row = db.prepare( "SELECT s.token,s.expires_at,u.id,u.username,u.email,u.created_at FROM sessions s JOIN users u ON u.id=s.user_id WHERE s.token=?" ).get(token); if (!row) return null; const now = Date.now(); if (row.expires_at < now) { db.prepare("DELETE FROM sessions WHERE token=?").run(token); return null; } if (row.expires_at - now < SESSION_TTL / 2) db.prepare("UPDATE sessions SET expires_at=? WHERE token=?").run(now + SESSION_TTL, token); return row; } function destroySession(db, token) { if (token) db.prepare("DELETE FROM sessions WHERE token=?").run(token); } /* Set-Cookie 字符串;secure=true 用于 HTTPS 部署(环境变量 COOKIE_SECURE=1) */ function sessionCookie(token, secure) { const parts = [ COOKIE_NAME + "=" + token, "Path=/", "HttpOnly", "SameSite=Lax", "Max-Age=" + Math.floor(SESSION_TTL / 1000), ]; if (secure) parts.push("Secure"); return parts.join("; "); } function clearCookie(secure) { return COOKIE_NAME + "=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0" + (secure ? "; Secure" : ""); } module.exports = { hashPassword, verifyPassword, createSession, getSessionUser, destroySession, sessionCookie, clearCookie, COOKIE_NAME };