trustbase-seller-backend 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/routes/onboard.html +60 -41
- package/routes/onboard.js +7 -5
- package/test-onboard-backup.js +18 -8
package/package.json
CHANGED
package/routes/onboard.html
CHANGED
|
@@ -148,9 +148,8 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
|
|
|
148
148
|
<div id="restore-msg" style="font-size:13px;margin-top:6px"></div>
|
|
149
149
|
</div>
|
|
150
150
|
<div id="backup-quiz" style="display:none;margin-top:10px;background:#FCFAF4;border:1px solid #EAE3D6;border-radius:12px;padding:14px 16px">
|
|
151
|
-
<div class="desc" style="margin-bottom:4px">确认你真的抄好了 —
|
|
151
|
+
<div class="desc" style="margin-bottom:4px">确认你真的抄好了 — 分 3 步, 一次填一个词 (答错不会提示正确答案):</div>
|
|
152
152
|
<div id="backup-questions"></div>
|
|
153
|
-
<button class="sbtn primary" style="margin-top:10px" onclick="checkBackupVerify()">提交验证</button>
|
|
154
153
|
</div>
|
|
155
154
|
<div id="backup-result"></div>
|
|
156
155
|
</div>
|
|
@@ -416,47 +415,74 @@ function renderProgress(s){
|
|
|
416
415
|
// 当前可做项高亮 (备份验证已过 → 持久打勾 (后端 tb_backup_ack 台账); 否则标当前可做)
|
|
417
416
|
if(s.backup_acked){ markStepDone('st-backup'); } else { document.getElementById('st-backup').className='step current'; }
|
|
418
417
|
}
|
|
419
|
-
// ----
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
|
|
418
|
+
// ---- 店铺安全卡回填验证 (2026-10-03, MetaMask 式抽词; 原则: 安全词不出本机) ----
|
|
419
|
+
// 分步式 (xiaomu 拍板): 抽 3 词位, 一次只问一个词, 每步单独验证 (即时反馈, 答错不累积)。
|
|
420
|
+
// 首次创建 (安全词还在页面内存 _mn) → 纯前端比对, 不调网络;
|
|
421
|
+
// 页面刷新后 → POST /api/onboard/verify-backup 单元素 positions (端点 ≤5 支持), 本机后端比对。
|
|
422
|
+
var _bq={positions:[],idx:0,wrong:0};
|
|
423
423
|
function startBackupVerify(){
|
|
424
424
|
// 随机抽 3 个词位 (1-24 不重复, 升序展示)
|
|
425
425
|
var pool=[]; for(var i=1;i<=24;i++) pool.push(i);
|
|
426
426
|
var pos=[];
|
|
427
427
|
while(pos.length<3){ pos.push(pool.splice(Math.floor(Math.random()*pool.length),1)[0]); }
|
|
428
428
|
pos.sort(function(a,b){return a-b;});
|
|
429
|
-
|
|
430
|
-
q.innerHTML=pos.map(function(p){
|
|
431
|
-
return '<label style="display:block;font-size:13px;color:#6B655B;margin:8px 0 4px">请按安全卡上抄写的填写第 '+p+' 个词</label>'
|
|
432
|
-
+'<input class="bq-word" data-pos="'+p+'" placeholder="第 '+p+' 个词" autocapitalize="off" autocomplete="off" spellcheck="false" style="width:100%;padding:10px 12px;border:1px solid #D4CDBF;border-radius:8px;font-size:15px;background:#fff">';
|
|
433
|
-
}).join('');
|
|
429
|
+
_bq={positions:pos,idx:0,wrong:0};
|
|
434
430
|
document.getElementById('backup-result').innerHTML='';
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
431
|
+
document.getElementById('backup-quiz').style.display='block';
|
|
432
|
+
renderBqStep();
|
|
433
|
+
}
|
|
434
|
+
function renderBqStep(){
|
|
435
|
+
var p=_bq.positions[_bq.idx];
|
|
436
|
+
var q=document.getElementById('backup-questions');
|
|
437
|
+
q.innerHTML='<div style="font-size:12px;color:#8A857B;margin-bottom:6px">第 '+(_bq.idx+1)+'/3 步</div>'
|
|
438
|
+
+'<label style="display:block;font-size:13px;color:#6B655B;margin:0 0 4px">请按安全卡上抄写的填写第 '+p+' 个词</label>'
|
|
439
|
+
+'<input class="bq-word" id="bq-input" data-pos="'+p+'" placeholder="第 '+p+' 个词" autocapitalize="off" autocomplete="off" spellcheck="false" style="width:100%;padding:12px;font-size:16px;border:1px solid #D4CDBF;border-radius:8px;background:#fff">'
|
|
440
|
+
+'<div style="display:flex;gap:8px;margin-top:10px">'
|
|
441
|
+
+'<button class="sbtn primary" style="flex:1;padding:12px;font-size:15px" onclick="checkBqStep()">'+(_bq.idx<2?'下一步':'完成验证')+'</button>'
|
|
442
|
+
+'<button class="sbtn" onclick="startBackupVerify()">返回重抽</button></div>'
|
|
443
|
+
+(_bq.wrong>=3?'<div style="margin-top:8px"><button class="sbtn" onclick="swapBqPosition()">这个词位填不对? 换一个词位</button></div>':'');
|
|
444
|
+
var inp=document.getElementById('bq-input');
|
|
445
|
+
inp.focus();
|
|
446
|
+
inp.onkeydown=function(e){ if(e.key==='Enter') checkBqStep(); };
|
|
438
447
|
}
|
|
439
|
-
async function
|
|
440
|
-
var
|
|
441
|
-
var
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
448
|
+
async function checkBqStep(){
|
|
449
|
+
var inp=document.getElementById('bq-input');
|
|
450
|
+
var w=(inp.value||'').trim();
|
|
451
|
+
if(!w) return backupResult(false,'请先填写这个词');
|
|
452
|
+
var p=Number(inp.getAttribute('data-pos'));
|
|
453
|
+
var correct=false;
|
|
454
|
+
// ① 内存路径: 安全词还在 (_mn) → 纯前端比对 (更快, 零网络)
|
|
445
455
|
if(_mn){
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
correct = _mn.split(/\s+/)[p-1].toLowerCase()===w.toLowerCase();
|
|
457
|
+
}else{
|
|
458
|
+
// ② 后端路径: 刷新后内存没有 → 本机后端比对 (安全词不出本机, 只送位置+词)
|
|
459
|
+
try{
|
|
460
|
+
var j=await readJson(await fetch('/api/onboard/verify-backup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({positions:[p],words:[w]})}));
|
|
461
|
+
if(j.ok) correct=true;
|
|
462
|
+
else if(j.error==='rate_limited') return backupResult(false, j.message||'操作太频繁, 请稍后再试');
|
|
463
|
+
else if(j.wrong_positions&&j.wrong_positions.length) correct=false;
|
|
464
|
+
else return backupResult(false, j.message||'验证失败, 请稍后再试');
|
|
465
|
+
}catch(e){ return backupResult(false,'服务暂时不可用,请刷新重试 ('+e.message+')'); }
|
|
466
|
+
}
|
|
467
|
+
if(correct){
|
|
468
|
+
_bq.idx++; _bq.wrong=0;
|
|
469
|
+
document.getElementById('backup-result').innerHTML='';
|
|
470
|
+
if(_bq.idx>=3){ await persistBackupAck(); return backupOk(); }
|
|
471
|
+
renderBqStep();
|
|
472
|
+
}else{
|
|
473
|
+
_bq.wrong++;
|
|
474
|
+
backupResult(false,'⚠️ 这个词不对 (不会提示正确答案), 请对照安全卡重新填');
|
|
475
|
+
if(_bq.wrong>=3) renderBqStep(); // 同一词位错 3 次 → 出现"换一个词位"
|
|
476
|
+
else { inp.value=''; inp.focus(); }
|
|
451
477
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
478
|
+
}
|
|
479
|
+
function swapBqPosition(){
|
|
480
|
+
// 换掉当前词位 (与其他 2 个不重复), 错次清零
|
|
481
|
+
var cand;
|
|
482
|
+
do{ cand=1+Math.floor(Math.random()*24); }while(_bq.positions.indexOf(cand)>=0);
|
|
483
|
+
_bq.positions[_bq.idx]=cand; _bq.wrong=0;
|
|
484
|
+
document.getElementById('backup-result').innerHTML='';
|
|
485
|
+
renderBqStep();
|
|
460
486
|
}
|
|
461
487
|
async function persistBackupAck(){
|
|
462
488
|
try{ await fetch('/api/onboard/backup-ack',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}); }catch(e){}
|
|
@@ -465,14 +491,7 @@ function backupOk(){
|
|
|
465
491
|
document.getElementById('backup-quiz').style.display='none';
|
|
466
492
|
markStepDone('st-backup'); // 步骤②打勾 (后端台账已持久化, 换浏览器仍绿)
|
|
467
493
|
backupResult(true,'✅ 备份已确认 — 你的店铺安全卡只有这一份, 请收好');
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
function backupWrong(wrong){
|
|
471
|
-
_bqTries++;
|
|
472
|
-
var tip='⚠️ 第 '+wrong.join('、')+' 个词不对 — 请对照安全卡检查 (不会提示正确答案), 已重新抽题';
|
|
473
|
-
if(_bqTries>=3) tip='⚠️ 第 '+wrong.join('、')+' 个词不对; 已错 3 次, 建议点上方"重新打印安全卡"逐词对照后再试';
|
|
474
|
-
startBackupVerify(); // 重新抽题
|
|
475
|
-
backupResult(false, tip);
|
|
494
|
+
_bq={positions:[],idx:0,wrong:0};
|
|
476
495
|
}
|
|
477
496
|
function backupResult(ok, text){
|
|
478
497
|
document.getElementById('backup-result').innerHTML=
|
package/routes/onboard.js
CHANGED
|
@@ -478,7 +478,7 @@ function createOnboardRouter(deps) {
|
|
|
478
478
|
db.prepare("INSERT INTO tb_backup_ack (id, acked_at) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET acked_at=excluded.acked_at")
|
|
479
479
|
.run(Math.floor(Date.now() / 1000));
|
|
480
480
|
}
|
|
481
|
-
const backupVerifyRate = new Map(); // ip →
|
|
481
|
+
const backupVerifyRate = new Map(); // ip → 30s 窗口内的尝试时间戳[] (分步式验证一步一调, 6 次/30s 足够 3 步 + 重试)
|
|
482
482
|
router.post("/verify-backup", (req, res) => {
|
|
483
483
|
try {
|
|
484
484
|
const b = req.body || {};
|
|
@@ -492,11 +492,13 @@ function createOnboardRouter(deps) {
|
|
|
492
492
|
return res.status(400).json({ ok: false, error: "invalid_param", message: "positions 必须是 1-24 的整数" });
|
|
493
493
|
}
|
|
494
494
|
const ip = req.ip || "local";
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
495
|
+
const nowMs = Date.now();
|
|
496
|
+
const arr = (backupVerifyRate.get(ip) || []).filter((t) => nowMs - t < 30000);
|
|
497
|
+
if (arr.length >= 6) {
|
|
498
|
+
return res.status(429).json({ ok: false, error: "rate_limited", message: "验证太频繁 (30 秒内最多 6 次), 请对照安全卡稍后再试" });
|
|
498
499
|
}
|
|
499
|
-
|
|
500
|
+
arr.push(nowMs);
|
|
501
|
+
backupVerifyRate.set(ip, arr);
|
|
500
502
|
let realWords;
|
|
501
503
|
try {
|
|
502
504
|
realWords = fs.readFileSync(basePaths.mnemonicFile(), "utf8").trim().split(/\s+/);
|
package/test-onboard-backup.js
CHANGED
|
@@ -83,15 +83,18 @@ test("verify-backup: 错词 → wrong_positions (不泄正确答案) + 大小写
|
|
|
83
83
|
} finally { srv.close(); }
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test("verify-backup:
|
|
86
|
+
test("verify-backup: 限流滑动窗口 6 次/30s (分步验证一步一调, 3 步+重试够用)", async () => {
|
|
87
87
|
const { srv, base } = await startServer();
|
|
88
88
|
try {
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// 单元素 positions (分步式每步一词)
|
|
90
|
+
const body = JSON.stringify({ positions: [1], words: ["economy"] });
|
|
91
|
+
for (let i = 0; i < 6; i++) {
|
|
92
|
+
const r = await fetch(base + "/api/onboard/verify-backup", { method: "POST", headers: H, body });
|
|
93
|
+
assert.strictEqual(r.status, 200, "第 " + (i + 1) + " 次放行");
|
|
94
|
+
}
|
|
95
|
+
const r7 = await fetch(base + "/api/onboard/verify-backup", { method: "POST", headers: H, body });
|
|
96
|
+
assert.strictEqual(r7.status, 429, "第 7 次 (30s 窗口内) 拒");
|
|
97
|
+
assert.strictEqual((await r7.json()).error, "rate_limited");
|
|
95
98
|
} finally { srv.close(); }
|
|
96
99
|
});
|
|
97
100
|
|
|
@@ -120,7 +123,7 @@ test("backup-ack: 纯前端比对路径落账 (不带密钥材料) + status done
|
|
|
120
123
|
} finally { srv.close(); }
|
|
121
124
|
});
|
|
122
125
|
|
|
123
|
-
test("页面含备份验证 UI (
|
|
126
|
+
test("页面含备份验证 UI (分步式: 单词位输入框/下一步/进度提示/换词位) + 内存路径分支", async () => {
|
|
124
127
|
const { srv, base } = await startServer();
|
|
125
128
|
try {
|
|
126
129
|
const html = await (await fetch(base + "/onboard/")).text();
|
|
@@ -129,6 +132,13 @@ test("页面含备份验证 UI (抽题/提交/结果显示) + 内存路径分支
|
|
|
129
132
|
assert.ok(html.includes("/api/onboard/verify-backup"), "后端比对路径");
|
|
130
133
|
assert.ok(html.includes("persistBackupAck"), "内存路径 ack 落账");
|
|
131
134
|
assert.ok(html.includes("我已抄好"), "大白话文案");
|
|
135
|
+
// 分步式 (2026-10-03): 一次一词 + 下一步 + 进度 + 重抽/换词位
|
|
136
|
+
assert.ok(html.includes("checkBqStep"), "单步验证函数");
|
|
137
|
+
assert.ok(html.includes("下一步"), "下一步按钮");
|
|
138
|
+
assert.ok(html.includes("/3 步"), "进度提示");
|
|
139
|
+
assert.ok(html.includes("返回重抽"), "返回重抽");
|
|
140
|
+
assert.ok(html.includes("swapBqPosition"), "换一个词位");
|
|
141
|
+
assert.ok(!html.includes("checkBackupVerify"), "旧一次性提交函数已移除");
|
|
132
142
|
} finally { srv.close(); }
|
|
133
143
|
});
|
|
134
144
|
|