a6f763ffd8
- 修复token泄露风险:改用X-Local-Creds-Token请求头传递token,从URL Hash读取set_token避免日志记录 - 重构全站认证逻辑:提取公用工具函数统一各服务的认证校验流程 - 升级投资账本工具:支持本地上传CSV和粘贴文本,移除硬编码路径,适配新参数格式 - 新增403/404/500标准错误响应文件,统一API错误返回格式 - 优化后端配置:关闭ETag,重构CORS中间件,新增全局错误捕获,限制敏感文件访问 - 优化web zen box的Service Worker,支持音频分片请求与断点续传 - 清理冗余前端代码,调整页面格式与权限控制
213 lines
8.0 KiB
JavaScript
213 lines
8.0 KiB
JavaScript
const Database = require('better-sqlite3')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
const crypto = require('crypto')
|
|
|
|
const dataDir = path.join(process.cwd(), 'data')
|
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
|
const dbPath = path.join(dataDir, 'language_behavior_lock.db')
|
|
const db = new Database(dbPath)
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS lbl_users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lbl_sessions (
|
|
token TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lbl_user_data (
|
|
username TEXT PRIMARY KEY,
|
|
records_json TEXT NOT NULL DEFAULT '[]',
|
|
keywords_json TEXT NOT NULL DEFAULT '[]',
|
|
draft_json TEXT,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_lbl_sessions_user ON lbl_sessions(username);
|
|
CREATE INDEX IF NOT EXISTS idx_lbl_sessions_exp ON lbl_sessions(expires_at);
|
|
`)
|
|
|
|
const now = () => Date.now()
|
|
const SESSION_MAX_AGE = 1 * 24 * 3600 * 1000
|
|
const COOKIE_NAME = 'language_behavior_lock_sid'
|
|
const DEFAULT_USER = { username: 'admin_lbl', password: process.env.LOCK_DEFAULT_PASSWORD || '' }
|
|
|
|
const parseCookie = cookieStr => {
|
|
const out = {}
|
|
String(cookieStr || '').split(/;\s*/).forEach(p => {
|
|
const idx = p.indexOf('=')
|
|
if (idx > 0) out[p.slice(0, idx)] = decodeURIComponent(p.slice(idx + 1))
|
|
})
|
|
return out
|
|
}
|
|
|
|
const ensureDefaultUser = () => {
|
|
const row = db.prepare('SELECT username FROM lbl_users WHERE username = ?').get(DEFAULT_USER.username)
|
|
if (!row) {
|
|
db.prepare('INSERT INTO lbl_users (username, password, created_at) VALUES (?, ?, ?)').run(DEFAULT_USER.username, DEFAULT_USER.password, now())
|
|
}
|
|
}
|
|
|
|
const cleanupSessions = () => {
|
|
db.prepare('DELETE FROM lbl_sessions WHERE expires_at <= ?').run(now())
|
|
}
|
|
|
|
const readDataOf = username => {
|
|
const row = db.prepare('SELECT records_json, keywords_json, draft_json FROM lbl_user_data WHERE username = ?').get(username)
|
|
if (!row) return { db: { records: [], keywords: [] }, draft: null }
|
|
let records = []
|
|
let keywords = []
|
|
let draft = null
|
|
try { records = JSON.parse(row.records_json || '[]') } catch {}
|
|
try { keywords = JSON.parse(row.keywords_json || '[]') } catch {}
|
|
try { draft = row.draft_json ? JSON.parse(row.draft_json) : null } catch {}
|
|
return {
|
|
db: {
|
|
records: Array.isArray(records) ? records : [],
|
|
keywords: Array.isArray(keywords) ? keywords : []
|
|
},
|
|
draft: draft && typeof draft === 'object' ? draft : null
|
|
}
|
|
}
|
|
|
|
const writeDataOf = (username, data) => {
|
|
const dbData = data && data.db && typeof data.db === 'object' ? data.db : {}
|
|
const records = Array.isArray(dbData.records) ? dbData.records : []
|
|
const keywords = Array.isArray(dbData.keywords) ? dbData.keywords : []
|
|
const draft = data && data.draft && typeof data.draft === 'object' ? data.draft : null
|
|
db.prepare(`
|
|
INSERT INTO lbl_user_data (username, records_json, keywords_json, draft_json, updated_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(username) DO UPDATE SET
|
|
records_json = excluded.records_json,
|
|
keywords_json = excluded.keywords_json,
|
|
draft_json = excluded.draft_json,
|
|
updated_at = excluded.updated_at
|
|
`).run(
|
|
username,
|
|
JSON.stringify(records),
|
|
JSON.stringify(keywords),
|
|
draft ? JSON.stringify(draft) : null,
|
|
now()
|
|
)
|
|
}
|
|
|
|
const getSessionUser = req => {
|
|
cleanupSessions()
|
|
const cookies = parseCookie(req.headers.cookie || '')
|
|
const token = String(cookies[COOKIE_NAME] || '')
|
|
if (!token) return ''
|
|
const row = db.prepare('SELECT username, expires_at FROM lbl_sessions WHERE token = ?').get(token)
|
|
if (!row) return ''
|
|
if (Number(row.expires_at) <= now()) {
|
|
db.prepare('DELETE FROM lbl_sessions WHERE token = ?').run(token)
|
|
return ''
|
|
}
|
|
return String(row.username || '')
|
|
}
|
|
|
|
const requireAuth = (req, res, next) => {
|
|
const username = getSessionUser(req)
|
|
if (!username) return res.status(401).json({ ok: false, error: 'unauthorized' })
|
|
req.lblUser = username
|
|
next()
|
|
}
|
|
|
|
const getLocalCredsToken = (req) => {
|
|
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
|
if (headerToken) return headerToken
|
|
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
|
return String(req.body.token || '').trim()
|
|
}
|
|
return ''
|
|
}
|
|
|
|
const bindRoutes = app => {
|
|
ensureDefaultUser()
|
|
|
|
app.get('/api/language_behavior_lock/health', (req, res) => {
|
|
res.json({ ok: true })
|
|
})
|
|
|
|
app.get('/api/language_behavior_lock/auth/local_creds', (req, res) => {
|
|
try {
|
|
const os = require('os')
|
|
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
|
|
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
|
|
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
|
|
try {
|
|
if (fs.existsSync(usersRoot)) {
|
|
fs.readdirSync(usersRoot).forEach(name => {
|
|
const dir = path.join(usersRoot, name)
|
|
if (dir !== userProfile && fs.existsSync(dir)) candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
|
|
})
|
|
}
|
|
} catch {}
|
|
let content = null
|
|
for (const p of candidates) { if (fs.existsSync(p)) { content = fs.readFileSync(p, 'utf-8'); break } }
|
|
if (!content) return res.json({ ok: true, creds: null })
|
|
const creds = {}
|
|
content.split(/\r?\n/).forEach(line => {
|
|
const t = line.trim()
|
|
if (!t || t.startsWith('#')) return
|
|
const idx = t.indexOf('=')
|
|
if (idx > 0) creds[t.slice(0, idx).trim()] = t.slice(idx + 1).trim()
|
|
})
|
|
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
|
|
if (serverToken) {
|
|
const clientToken = getLocalCredsToken(req)
|
|
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
|
|
}
|
|
const u = creds.LBL_USERNAME || ''
|
|
const p = creds.LBL_PASSWORD || ''
|
|
if (!u && !p) return res.json({ ok: true, creds: null })
|
|
res.json({ ok: true, creds: { username: u, password: p } })
|
|
} catch { res.json({ ok: true, creds: null }) }
|
|
})
|
|
|
|
app.post('/api/language_behavior_lock/auth/login', (req, res) => {
|
|
const username = String((req.body && req.body.username) || '').trim()
|
|
const password = String((req.body && req.body.password) || '')
|
|
if (!username || !password) return res.status(400).json({ ok: false, error: 'invalid_credentials' })
|
|
const row = db.prepare('SELECT username, password FROM lbl_users WHERE username = ?').get(username)
|
|
if (!row || String(row.password || '') !== password) return res.status(401).json({ ok: false, error: 'invalid_credentials' })
|
|
const token = crypto.randomBytes(24).toString('hex')
|
|
const createdAt = now()
|
|
const expiresAt = createdAt + SESSION_MAX_AGE
|
|
db.prepare('INSERT INTO lbl_sessions (token, username, created_at, expires_at) VALUES (?, ?, ?, ?)').run(token, username, createdAt, expiresAt)
|
|
res.cookie(COOKIE_NAME, token, { httpOnly: true, sameSite: 'lax', maxAge: SESSION_MAX_AGE })
|
|
res.json({ ok: true, username })
|
|
})
|
|
|
|
app.post('/api/language_behavior_lock/auth/logout', (req, res) => {
|
|
const cookies = parseCookie(req.headers.cookie || '')
|
|
const token = String(cookies[COOKIE_NAME] || '')
|
|
if (token) db.prepare('DELETE FROM lbl_sessions WHERE token = ?').run(token)
|
|
res.cookie(COOKIE_NAME, '', { httpOnly: true, sameSite: 'lax', maxAge: 0 })
|
|
res.json({ ok: true })
|
|
})
|
|
|
|
app.get('/api/language_behavior_lock/auth/me', (req, res) => {
|
|
const username = getSessionUser(req)
|
|
if (!username) return res.status(401).json({ ok: false, error: 'unauthorized' })
|
|
res.json({ ok: true, username })
|
|
})
|
|
|
|
app.get('/api/language_behavior_lock/data', requireAuth, (req, res) => {
|
|
const payload = readDataOf(req.lblUser)
|
|
res.json({ ok: true, ...payload })
|
|
})
|
|
|
|
app.put('/api/language_behavior_lock/data', requireAuth, (req, res) => {
|
|
writeDataOf(req.lblUser, req.body || {})
|
|
res.json({ ok: true })
|
|
})
|
|
}
|
|
|
|
module.exports = { bindRoutes }
|