security: 改密即吊销全部旧令牌 + 登录防爆破 + COS 异地备份(环境变量开关) + 启动失败重试按钮

This commit is contained in:
root
2026-10-17 10:59:25 +08:00
parent 56a60d5dc1
commit 08b0317d35
8 changed files with 133 additions and 18 deletions
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-C-z1ujqe.js","./index-DT_fURyb.js","./index-2EGKD3r-.css"])))=>i.map(i=>d[i]);
import{r as s,_ as i}from"./index-DT_fURyb.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-C-z1ujqe.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-DtE1VpZK.js","./index-D6jOKt8Z.js","./index-2EGKD3r-.css"])))=>i.map(i=>d[i]);
import{r as s,_ as i}from"./index-D6jOKt8Z.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-DtE1VpZK.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
<title>实习工作日志</title>
<script type="module" crossorigin src="./assets/index-DT_fURyb.js"></script>
<script type="module" crossorigin src="./assets/index-D6jOKt8Z.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-2EGKD3r-.css">
</head>
<body>
+8 -1
View File
@@ -26,7 +26,8 @@ async function main() {
check('错误密码登录 401', bad.status === 401, bad.status)
const ok = await post('/api/auth/login', { username: 'audit_a', password: 'aapass123' })
check('正确密码登录 200', ok.status === 200 && !!ok.body.token, ok.status)
const tokA = reg.body.token, tokA2 = ok.body.token
let tokA = reg.body.token
const tokA2 = ok.body.token
const noAuth = await post('/api/sync', { tables: {} })
check('无 token 同步被拒 401', noAuth.status === 401, noAuth.status)
const regB = await post('/api/auth/register', { username: 'audit_b', password: 'bbpass123' })
@@ -85,6 +86,10 @@ async function main() {
check('旧密码立即失效 401', oldLogin.status === 401, oldLogin.status)
const newLogin = await post('/api/auth/login', { username: 'audit_a', password: 'aapass456' })
check('新密码登录 200', newLogin.status === 200, newLogin.status)
// 改密后旧令牌立即失效(会话吊销)
const oldTokSync = await post('/api/sync', { tables: {} }, tokA)
check('改密后旧 token 同步被拒 401', oldTokSync.status === 401, oldTokSync.status)
tokA = newLogin.body.token
const chgWrong = await post('/api/auth/password', { oldPassword: 'zzz', newPassword: 'whatever1' }, tokA)
check('错误旧密码改密被拒 400', chgWrong.status === 400, chgWrong.status)
@@ -123,6 +128,8 @@ async function main() {
check('注册表仅存哈希不存明文密码', !!aRec && !JSON.stringify(aRec).includes('aapass'))
await new Promise(r => globalThis.setTimeout(r, 400))
check('启动自动备份已生成', fsMod.existsSync('server/data/backups/' + new Date().toISOString().slice(0, 10) + '/registry.json'))
const hInfo = await fetch(BASE + '/api/health').then(x => x.json()).catch(() => ({}))
check('health 暴露备份状态', hInfo && hInfo.backup === 'local-only', hInfo.backup)
const d1 = await post('/api/auth/delete', {}, tokA)
check('删除账号 200', d1.status === 200, d1.body)
+6
View File
@@ -15,6 +15,12 @@ export function showFatalError(title: string, detail: string): void {
const box = document.createElement('div')
box.style.cssText = 'border:1px solid #E5484D;border-radius:8px;padding:12px;margin-bottom:12px;word-break:break-all;white-space:pre-wrap;'
box.textContent = title + '\n' + detail
// 重试入口:纯在线模式下连不上服务器时(如启动失败),一键重载而不是干等着重启应用
const retry = document.createElement('button')
retry.textContent = '重新加载'
retry.style.cssText = 'margin:-4px 0 12px;padding:6px 16px;border:1px solid #E5484D;border-radius:6px;background:#FFFFFF;color:#E5484D;font-size:13px;cursor:pointer;'
retry.addEventListener('click', () => { try { location.reload() } catch { /* 忽略 */ } })
el.appendChild(box)
el.appendChild(retry)
} catch { /* 浮层失败时保持静默 */ }
}