architecture: 数据唯一权威迁至服务器——本地降级为实时缓存,写后即时推送+快照重建;账号登录转为云端原生(新增 /api/auth/password、/api/auth/delete、每日自动备份)

This commit is contained in:
root
2026-10-16 16:13:38 +08:00
parent 7517eb0ad9
commit e5f99d46d8
16 changed files with 641 additions and 478 deletions
+59 -1
View File
@@ -46,7 +46,7 @@ const SECRET = readJson('secret.key', { secret: 'fallback-secret' }).secret
const registryFile = 'registry.json'
let registry = readJson(registryFile, { users: [] })
function saveRegistry() { writeJson(registryFile, registry) }
function saveRegistry() { writeJson(registryFile, registry); try { doBackup() } catch { /* 忽略 */ } }
function userFile(userId) { return path.join(DATA_DIR, 'users', userId + '.json') }
function loadUserData(userId) {
@@ -196,6 +196,36 @@ async function handle(req, res) {
return
}
// 修改登录密码(验证旧密码后重置盐与哈希)
if (req.method === 'POST' && route === '/api/auth/password') {
const auth = verifyToken(String(req.headers['authorization'] || '').replace(/^Bearer\s+/i, '').trim())
if (!auth) { json(res, 401, { error: '未认证' }); return }
const body = await readBody(req)
const rec = registry.users.find(u => u.id === auth.userId)
if (!rec) { json(res, 404, { error: '用户不存在' }); return }
if (pbkdf2Hash(String(body.oldPassword || ''), rec.pwd_salt) !== rec.pwd_hash) { json(res, 400, { error: '旧密码不正确' }); return }
if (String(body.newPassword || '').length < 6) { json(res, 400, { error: '新密码至少 6 位' }); return }
rec.pwd_salt = crypto.randomBytes(16).toString('hex')
rec.pwd_hash = pbkdf2Hash(String(body.newPassword), rec.pwd_salt)
rec.pwd_changed_at = new Date().toISOString()
saveRegistry()
json(res, 200, { ok: true })
return
}
// 删除云端账号:注册表与全部业务数据一并清除(不可恢复,客户端已做二次确认)
if (req.method === 'POST' && route === '/api/auth/delete') {
const auth = verifyToken(String(req.headers['authorization'] || '').replace(/^Bearer\s+/i, '').trim())
if (!auth) { json(res, 401, { error: '未认证' }); return }
const idx = registry.users.findIndex(u => u.id === auth.userId)
if (idx >= 0) { registry.users.splice(idx, 1); saveRegistry() }
try { fs.rmSync(userFile(auth.userId), { force: true }) } catch { /* 忽略 */ }
for (const sseRes of (sseClients.get(auth.userId) || [])) { try { sseRes.end() } catch { /* 忽略 */ } }
sseClients.delete(auth.userId)
json(res, 200, { ok: true })
return
}
if (req.method === 'POST' && route === '/api/auth/register') {
const body = await readBody(req)
const username = String(body.username || '').trim()
@@ -319,6 +349,34 @@ async function handle(req, res) {
json(res, 404, { error: 'not found' })
}
// ---------- 自动备份:服务器是唯一数据源,必须自带兜底 ----------
function doBackup() {
try {
const day = new Date().toISOString().slice(0, 10)
const dir = path.join(DATA_DIR, 'backups', day)
fs.mkdirSync(path.join(dir, 'users'), { recursive: true })
try { fs.copyFileSync(path.join(DATA_DIR, registryFile), path.join(dir, 'registry.json')) } catch { /* 忽略 */ }
try { fs.copyFileSync(path.join(DATA_DIR, 'secret.key'), path.join(dir, 'secret.key')) } catch { /* 忽略 */ }
try {
const usersDir = path.join(DATA_DIR, 'users')
if (fs.existsSync(usersDir)) {
for (const f of fs.readdirSync(usersDir)) {
if (f.endsWith('.json')) fs.copyFileSync(path.join(usersDir, f), path.join(dir, 'users', f))
}
}
} catch { /* 忽略 */ }
// 保留最近 14 天
try {
const backupsRoot = path.join(DATA_DIR, 'backups')
const days = fs.readdirSync(backupsRoot).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort()
while (days.length > 14) { const old = days.shift(); try { fs.rmSync(path.join(backupsRoot, old), { recursive: true, force: true }) } catch { /* 忽略 */ } }
} catch { /* 忽略 */ }
} catch (e) { console.error('backup failed:', e && e.message) }
}
doBackup()
setInterval(doBackup, 24 * 60 * 60 * 1000)
// ---------- HTTP 服务 ----------
const server = http.createServer((req, res) => {
handle(req, res).catch(err => {
json(res, 500, { error: err.message || 'server error' })