- server/:node:http+node:sqlite 零依赖后端(users/sessions/user_stats/codex_unlocks/runs 五表) - 安全:scrypt密码哈希/服务端session/全prepared statement/输入白名单/限流/CSP等安全头/静态白名单防穿越 - 前端:o-auth登陆注册 + o-home飞行员大厅(排行榜Top100三排序+图鉴展柜背景:随机3主3副2僚机+刷怪巡游) - 图鉴改为收集点亮制(未收集=剪影+???),服务端codex_unlocks与本地并集同步 - 修复:回标题/大厅后背景卡在上场残影——render按battleUI门控+离场G=null;空闲=星空 - 结算上报:endRun/rush胜利两处submitRun,Boss击杀计数入排行榜 - playwright全流程验证通过(注册/登陆/游戏/结算/图鉴/安全注入/限流);游戏视觉未验收(用户亲自)
70 lines
2.6 KiB
JavaScript
70 lines
2.6 KiB
JavaScript
"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 };
|