security: 每人一把口令(口令即身份) + 随机会话token + 无配置拒绝启动 + 爆破节流
- STORY_WEB_PASSWORD(默认story) 废弃 → STORY_WEB_USERS=名字1:口令1,名字2:口令2; 未配置/口令<8位/口令或用户名重复 → 启动即退出,杜绝弱默认口令裸奔 - cookie 不再存口令原文:登录发 secrets.token_urlsafe(32) 随机token, 会话存 SQLite sessions 表(30天);登出删token;从 USERS 移除某人=吊销其全部会话 - updated_by 改由服务端按会话身份填写,前端自报 by 不再可信;登录框去掉昵称字段 - 登录失败全局递增节流(最多sleep 5s),口令比较用 secrets.compare_digest - Dockerfile/compose 移除一切口令默认值;compose 未设 STORY_WEB_USERS 直接报错 - 顺手修 playtest.js 走位/动画/out_ref 行未转义的存储型XSS(esc补齐)
This commit is contained in:
@ -25,10 +25,11 @@
|
||||
function hideLogin() { $("login").style.display = "none"; }
|
||||
|
||||
$("login-btn").onclick = async () => {
|
||||
const pass = $("login-pass").value, name = $("login-name").value.trim();
|
||||
const pass = $("login-pass").value;
|
||||
const r = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: pass }) });
|
||||
if (!r.ok) { $("login-err").textContent = "口令错误"; return; }
|
||||
if (name) { App.by = name; localStorage.setItem("story_by", name); }
|
||||
const d = await r.json(); // 身份由服务端按口令认定(口令即身份)
|
||||
App.by = d.user || "?"; localStorage.setItem("story_by", App.by);
|
||||
hideLogin(); init();
|
||||
};
|
||||
$("login-pass").onkeydown = e => { if (e.key === "Enter") $("login-btn").click(); };
|
||||
|
||||
@ -13,9 +13,8 @@
|
||||
<div id="login" class="overlay">
|
||||
<div class="login-box">
|
||||
<h2>剧情事件协作编辑器</h2>
|
||||
<p class="hint">输入共享口令进入</p>
|
||||
<input id="login-pass" type="password" placeholder="共享口令" autocomplete="off">
|
||||
<input id="login-name" type="text" placeholder="你的昵称(用于记录改动者)">
|
||||
<p class="hint">输入你的专属口令进入(口令即身份,改动记录自动署名)</p>
|
||||
<input id="login-pass" type="password" placeholder="专属口令" autocomplete="off">
|
||||
<button id="login-btn">进入</button>
|
||||
<div id="login-err" class="err"></div>
|
||||
</div>
|
||||
|
||||
@ -90,10 +90,10 @@
|
||||
const k = n.kind;
|
||||
if (k === "narration") { add("spk", "<b>" + esc(nameOf(n.speaker || "P1")) + "</b>:" + esc(n.text)); loc = advance(loc); }
|
||||
else if (k === "dialogue") { add("spk", "<b>" + esc(nameOf(n.speaker)) + "</b>:" + esc(n.text)); loc = advance(loc); }
|
||||
else if (k === "move") { add("sys", "〔走位〕" + nameOf(n.actor) + " → " + (n.to || "")); loc = advance(loc); }
|
||||
else if (k === "anim") { add("sys", "〔动画〕" + nameOf(n.actor) + " " + (n.ani || "")); loc = advance(loc); }
|
||||
else if (k === "move") { add("sys", "〔走位〕" + esc(nameOf(n.actor)) + " → " + esc(n.to || "")); loc = advance(loc); }
|
||||
else if (k === "anim") { add("sys", "〔动画〕" + esc(nameOf(n.actor)) + " " + esc(n.ani || "")); loc = advance(loc); }
|
||||
else if (k === "reward") { applyGrants(n.grants, "结算"); add("sys", "〔奖励结算〕"); loc = advance(loc); }
|
||||
else if (k === "out_ref") { add("sys", "〔进入子序列 " + n.ref + "〕"); loc = enterSeq(n.ref, n.next); }
|
||||
else if (k === "out_ref") { add("sys", "〔进入子序列 " + esc(n.ref) + "〕"); loc = enterSeq(n.ref, n.next); }
|
||||
else if (k === "choice" || k === "choice_once") { renderChoice(n); return; }
|
||||
else if (k === "random") { renderRandom(n); return; }
|
||||
else if (k === "fight") { renderFight(n); return; }
|
||||
|
||||
Reference in New Issue
Block a user