feat: 重构认证流程,升级工具功能并优化后端配置

- 修复token泄露风险:改用X-Local-Creds-Token请求头传递token,从URL Hash读取set_token避免日志记录
- 重构全站认证逻辑:提取公用工具函数统一各服务的认证校验流程
- 升级投资账本工具:支持本地上传CSV和粘贴文本,移除硬编码路径,适配新参数格式
- 新增403/404/500标准错误响应文件,统一API错误返回格式
- 优化后端配置:关闭ETag,重构CORS中间件,新增全局错误捕获,限制敏感文件访问
- 优化web zen box的Service Worker,支持音频分片请求与断点续传
- 清理冗余前端代码,调整页面格式与权限控制
This commit is contained in:
yangxiangyuan
2026-08-01 21:50:05 +08:00
parent 2143adb3bb
commit a6f763ffd8
26 changed files with 304 additions and 72 deletions
+1
View File
@@ -0,0 +1 @@
{"ok":false,"error":"forbidden"}
+1
View File
@@ -0,0 +1 @@
{"ok":false,"error":"not_found"}
+1
View File
@@ -0,0 +1 @@
{"ok":false,"error":"internal_error"}
+7 -2
View File
@@ -51,10 +51,15 @@ window.cnDateStr = cnDateStr
;(function handleSetToken() {
try {
const u = new URL(window.location.href)
const token = u.searchParams.get('set_token')
const hashRaw = String(u.hash || '').replace(/^#/, '')
if (!/(^|[?&])set_token=/.test(hashRaw)) return
const hashParams = new URLSearchParams(hashRaw.startsWith('?') ? hashRaw.slice(1) : hashRaw)
const token = hashParams.get('set_token')
if (token) {
localStorage.setItem('toolbox_creds_token', token)
u.searchParams.delete('set_token')
hashParams.delete('set_token')
const nextHash = hashParams.toString()
u.hash = nextHash ? `#${nextHash}` : ''
window.history.replaceState(null, '', u.toString())
}
} catch {}
+5 -2
View File
@@ -1292,8 +1292,11 @@
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/ai-lib/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const r = await fetch('/api/ai-lib/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) el('login-user').value = d.creds.username
+5 -2
View File
@@ -2095,8 +2095,11 @@ const cn = (() => {
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/cloud_notes/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const r = await fetch('/api/cloud_notes/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) el('cn-username').value = d.creds.username
+5 -2
View File
@@ -1064,8 +1064,11 @@ const cs = (() => {
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/cloud_sheets/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const r = await fetch('/api/cloud_sheets/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) el('cs-username').value = d.creds.username
@@ -1630,8 +1630,11 @@ SHA256: ${esc(result.sha256 || '-')}
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/doc_cloud_keeper/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const r = await fetch('/api/doc_cloud_keeper/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) el('dck-username').value = d.creds.username
+1 -1
View File
@@ -16,7 +16,7 @@
<div class="dck-stats">
<span id="dck-stat-total">文件数: 0</span>
<span id="dck-stat-size">占用: 0 B</span>
<span id="dck-stat-trash">回收站: 0</span><a href="DocHelper\DocHelper.zip">⚒</a>
<span id="dck-stat-trash">回收站: 0</span>
</div>
</div>
<div id="dck-header-actions" class="dck-actions" style="display:none">
+4 -2
View File
@@ -11,8 +11,10 @@
<section class="panel">
<h1>投资事件账本 V1</h1>
<div class="form-grid">
<label>CSV 文件路径</label>
<input id="filePath" type="text" value="d:\Trae_Files\TRAE-Toolbox\temp\工作簿1.csv" />
<label>CSV 文件</label>
<input id="fileInput" type="file" accept=".csv,.txt,text/csv,text/plain" />
<label>CSV 文本</label>
<textarea id="rawText" rows="5" placeholder="可直接粘贴 CSV 原文;如果已选择文件,这里可以留空。"></textarea>
<label>标的代码</label>
<input id="symbol" type="text" value="510500" />
</div>
@@ -228,12 +228,22 @@ const saveModal = () => {
refreshAll()
}
const readSelectedFileText = async () => {
const input = $('fileInput')
const file = input && input.files && input.files[0]
if (!file) return ''
return file.text()
}
const parseFile = async () => {
try {
setStatus('解析中...')
$('btnParse').disabled = true
const fileText = await readSelectedFileText()
const rawText = String((fileText || $('rawText').value || '')).trim()
if (!rawText) throw new Error('请选择 CSV 文件,或粘贴 CSV 文本')
const payload = {
file_path: $('filePath').value.trim(),
raw_text: rawText,
symbol: $('symbol').value.trim()
}
const resp = await fetch('/api/investment_ledger/parse', {
@@ -2,10 +2,13 @@
;(function handleSetToken() {
try {
const u = new URL(window.location.href)
const token = u.searchParams.get('set_token')
const hashRaw = String(u.hash || '').replace(/^#/, '')
const hashParams = new URLSearchParams(hashRaw.startsWith('?') ? hashRaw.slice(1) : hashRaw)
const token = hashParams.get('set_token')
if (token) {
localStorage.setItem('toolbox_creds_token', token)
u.searchParams.delete('set_token')
hashParams.delete('set_token')
u.hash = hashParams.toString() ? `#${hashParams.toString()}` : ''
window.history.replaceState(null, '', u.toString())
}
} catch {}
@@ -1057,8 +1060,11 @@
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/language_behavior_lock/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const r = await fetch('/api/language_behavior_lock/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) $('login-username').value = d.creds.username
@@ -127,7 +127,11 @@
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
if (!token) return
const r = await apiGet(API + '/auth/local_creds?token=' + encodeURIComponent(token))
const r = await fetch(API + '/auth/local_creds', {
headers: { 'X-Local-Creds-Token': token },
credentials: 'same-origin',
cache: 'no-store'
}).then(resp => resp.json())
if (r && r.ok && r.creds) {
if (r.creds.username) $('#loginUsername').value = r.creds.username
if (r.creds.password) $('#loginPassword').value = r.creds.password
@@ -146,12 +150,17 @@
const main = async () => {
initTheme()
// ===== set_token 处理:仅存入 localStorage,不自动登录 =====
// ===== set_token 仅允许从 hash 读取,避免进入服务端日志 =====
try {
const params = new URLSearchParams(location.search)
const setToken = params.get('set_token')
const currentUrl = new URL(window.location.href)
const hashRaw = String(currentUrl.hash || '').replace(/^#/, '')
const hashParams = new URLSearchParams(hashRaw.startsWith('?') ? hashRaw.slice(1) : hashRaw)
const setToken = hashParams.get('set_token')
if (setToken) {
localStorage.setItem('toolbox_creds_token', setToken)
hashParams.delete('set_token')
currentUrl.hash = hashParams.toString() ? `#${hashParams.toString()}` : ''
history.replaceState(null, document.title, currentUrl.toString())
}
} catch {}
+10 -4
View File
@@ -980,12 +980,17 @@ const createFolder = async () => {
}
const init = async () => {
// ===== set_token 处理:必须在 clearUrlQuery 之前 =====
// ===== set_token 仅允许从 hash 读取,避免进入服务端日志 =====
try {
const params = new URLSearchParams(window.location.search)
const setToken = params.get('set_token')
const currentUrl = new URL(window.location.href)
const hashRaw = String(currentUrl.hash || '').replace(/^#/, '')
const hashParams = new URLSearchParams(hashRaw.startsWith('?') ? hashRaw.slice(1) : hashRaw)
const setToken = hashParams.get('set_token')
if (setToken) {
localStorage.setItem('toolbox_creds_token', setToken)
hashParams.delete('set_token')
currentUrl.hash = hashParams.toString() ? `#${hashParams.toString()}` : ''
window.history.replaceState(null, '', currentUrl.toString())
}
} catch {}
@@ -1038,7 +1043,8 @@ const init = async () => {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
if (!token) return
const res = await fetch(apiBase + '/auth/local_creds?token=' + encodeURIComponent(token), {
const res = await fetch(apiBase + '/auth/local_creds', {
headers: { 'X-Local-Creds-Token': token },
credentials: 'same-origin',
cache: 'no-store'
})
+10 -4
View File
@@ -2,10 +2,13 @@
;(function handleSetToken() {
try {
const u = new URL(window.location.href)
const token = u.searchParams.get('set_token')
const hashRaw = String(u.hash || '').replace(/^#/, '')
const hashParams = new URLSearchParams(hashRaw.startsWith('?') ? hashRaw.slice(1) : hashRaw)
const token = hashParams.get('set_token')
if (token) {
localStorage.setItem('toolbox_creds_token', token)
u.searchParams.delete('set_token')
hashParams.delete('set_token')
u.hash = hashParams.toString() ? `#${hashParams.toString()}` : ''
window.history.replaceState(null, '', u.toString())
}
} catch {}
@@ -2002,8 +2005,11 @@
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = '/api/rational_lock/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const r = await fetch('/api/rational_lock/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const d = await r.json().catch(() => null)
if (d && d.ok && d.creds) {
if (d.creds.username) $('login-username').value = d.creds.username
+55 -8
View File
@@ -1,6 +1,58 @@
const CACHE = 'music-box-v4';
const CACHE = 'music-box-v5';
const AUDIO_CACHE = 'zen-audio-cache';
function isAudioRequest(request) {
return request.url.includes('.mp3') || request.url.includes('oss');
}
async function buildPartialAudioResponse(request, cachedResponse) {
const rangeHeader = request.headers.get('range');
if (!rangeHeader) return cachedResponse;
const match = /bytes=(\d*)-(\d*)/.exec(rangeHeader);
if (!match) return cachedResponse;
const blob = await cachedResponse.blob();
const size = blob.size;
let start = match[1] ? Number(match[1]) : 0;
let end = match[2] ? Number(match[2]) : size - 1;
if (!Number.isFinite(start) || start < 0) start = 0;
if (!Number.isFinite(end) || end >= size) end = size - 1;
if (start >= size || start > end) {
return new Response(null, {
status: 416,
headers: {
'Accept-Ranges': 'bytes',
'Content-Range': `bytes */${size}`
}
});
}
const partialBlob = blob.slice(start, end + 1, blob.type || cachedResponse.headers.get('content-type') || 'audio/mpeg');
return new Response(partialBlob, {
status: 206,
headers: {
'Accept-Ranges': 'bytes',
'Content-Length': String(end - start + 1),
'Content-Range': `bytes ${start}-${end}/${size}`,
'Content-Type': partialBlob.type || cachedResponse.headers.get('content-type') || 'audio/mpeg'
}
});
}
async function handleAudioRequest(request) {
const audioCache = await caches.open(AUDIO_CACHE);
const cached = await audioCache.match(request.url);
if (cached) {
return buildPartialAudioResponse(request, cached);
}
return fetch(request);
}
self.addEventListener('install', e => {
self.skipWaiting();
e.waitUntil(
@@ -25,14 +77,9 @@ self.addEventListener('activate', e => {
self.addEventListener('fetch', e => {
// 音频文件:优先命中 zen-audio-cache(离线播放必需),否则走网络
if (e.request.url.includes('.mp3') || e.request.url.includes('oss')) {
if (isAudioRequest(e.request)) {
e.respondWith(
caches.open(AUDIO_CACHE).then(audioCache =>
audioCache.match(e.request).then(cached => {
if (cached) return cached;
return fetch(e.request);
})
).catch(() => fetch(e.request))
handleAudioRequest(e.request).catch(() => fetch(e.request))
);
return;
}
+5 -2
View File
@@ -892,8 +892,11 @@
;(async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
const url = apiBase + '/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
const res = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
const res = await fetch(apiBase + '/auth/local_creds', {
headers: token ? { 'X-Local-Creds-Token': token } : {},
credentials: 'same-origin',
cache: 'no-store'
})
const data = await res.json().catch(() => null)
if (data && data.ok && data.creds) {
if (data.creds.username) el('login-user').value = data.creds.username
+10 -1
View File
@@ -30,6 +30,15 @@ const deriveKey = () => {
}
return _key
}
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 enc = (plain) => {
const key = deriveKey()
const iv = crypto.randomBytes(12)
@@ -431,7 +440,7 @@ const bindRoutes = (app) => {
})
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (serverToken) {
const clientToken = String(req.query.token || '')
const clientToken = getLocalCredsToken(req)
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
}
const u = creds.AILIB_USERNAME || ''
+76 -13
View File
@@ -96,6 +96,7 @@ app.use((req, res, next) => {
// 移除 X-Powered-By 响应头(避免泄露 Express 版本信息)
app.disable('x-powered-by')
app.set('etag', false)
// CORS 白名单限制 — 仅允许指定域名跨域访问 API
const allowedOrigins = [
@@ -106,18 +107,33 @@ const allowedOrigins = [
'http://localhost:8081',
'http://127.0.0.1:8081'
]
app.use(cors({
origin: function (origin, callback) {
// 允许无 origin 的请求(如 curl、服务器端调用)
if (!origin) return callback(null, true)
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
const isAllowedCorsOrigin = (origin) => !origin || allowedOrigins.includes(origin)
app.use((req, res, next) => {
const origin = String(req.headers.origin || '').trim()
const isAllowed = isAllowedCorsOrigin(origin)
if (origin && isAllowed) {
res.set('Access-Control-Allow-Origin', origin)
res.vary('Origin')
res.set('Access-Control-Allow-Credentials', 'true')
}
if (req.method === 'OPTIONS') {
if (!origin || isAllowed) {
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-Local-Creds-Token')
res.set('Access-Control-Allow-Credentials', 'true')
return res.status(204).end()
}
},
credentials: true
}))
return res.status(403).json({ ok: false, error: 'cors_origin_denied' })
}
if (origin && !isAllowed) {
return res.status(403).json({ ok: false, error: 'cors_origin_denied' })
}
return next()
})
// 强制 HTTPS:HTTP 请求返回 301 跳转到 HTTPS
app.use((req, res, next) => {
@@ -165,10 +181,23 @@ const readLocalCreds = () => {
} catch { return {} }
}
const getLocalCredsClientToken = (req) => {
try {
const headerToken = String(req.headers['x-local-creds-token'] || req.headers['x-toolbox-creds-token'] || '').trim()
if (headerToken) return headerToken
const authHeader = String(req.headers.authorization || '').trim()
if (authHeader.startsWith('Bearer ')) return authHeader.slice(7).trim()
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
return String(req.body.token || '').trim()
}
} catch {}
return ''
}
const checkLocalCredsToken = (req, localCreds) => {
const serverToken = String(localCreds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (!serverToken) return true
const clientToken = String(req.query.token || '')
const clientToken = getLocalCredsClientToken(req)
return clientToken === serverToken
}
@@ -3350,6 +3379,10 @@ app.get('/api/doc_cloud_keeper/auth/local_creds', (req, res) => {
res.json({ ok: true, creds: { username: u, password: p } })
} catch { sendNoCreds(res) }
})
app.use('/tools/doc_cloud_keeper/DocHelper', (req, res, next) => {
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(404).send('Not Found')
})
app.get('/tools/doc_cloud_keeper', (req, res, next) => {
try {
const token = String(req.query.token || '').trim()
@@ -6162,6 +6195,16 @@ app.use('/tools/web_order_box', (req, res, next) => {
next()
})
app.use('/tools/web_order_box', express.static(path.join(process.cwd(), 'public', 'tools', 'web_order_box')))
app.use('/tools/investment_ledger', (req, res, next) => {
try {
if (hasNavAuthRoot(req)) return next()
return res.status(401).send('未授权')
} catch { return res.status(401).send('未授权') }
})
app.get(['/package.json', '/package-lock.json', '/pnpm-lock.yaml', '/yarn.lock', '/robots.txt'], (_req, res) => {
res.status(404).send('Not Found')
})
app.get('/api/zen_box/playlists', (req, res) => {
try {
@@ -6623,7 +6666,12 @@ ensureWeeklyEmbedLoaded()
ensurePscLoaded()
aiLib.bindRoutes(app)
expense.bindRoutes(app)
investmentLedger.bindRoutes(app)
investmentLedger.bindRoutes(app, {
requireAuth: (req, res, next) => {
if (hasNavAuthRoot(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
}
})
styleCheck.bindRoutes(app)
globalNews.bindRoutes(app)
dataGateway.bindRoutes(app)
@@ -6731,6 +6779,21 @@ app.get('/api/yuanzhupai/sessions/:id', (req, res) => {
}
})
app.use((err, req, res, next) => {
try {
logJSON('http.unhandled.error', {
path: req && req.path,
method: req && req.method,
error: String(err && err.message ? err.message : err)
}, 'server')
} catch {}
if (res.headersSent) return next(err)
if (req && req.path && req.path.startsWith('/api/')) {
return res.status(500).json({ ok: false, error: 'internal_error' })
}
return res.status(500).send('服务器开小差了')
})
// 404 处理:覆盖 Express 默认 "Cannot GET" 错误页,避免泄露技术栈特征
app.use((req, res) => {
res.status(404).json({ ok: false, error: 'not_found' })
+19 -11
View File
@@ -2,6 +2,7 @@ const fs = require('fs')
const path = require('path')
const iconv = require('iconv-lite')
const { logJSON } = require('./logger')
const INTERNAL_ERROR = 'internal_error'
const readTextWithFallback = filePath => {
const buf = fs.readFileSync(filePath)
@@ -249,7 +250,11 @@ const summarizeEvents = events => {
}
}
const bindRoutes = app => {
const sendError = (res, status, error) => {
res.status(status).json({ ok: false, error })
}
const bindRoutes = (app, options = {}) => {
app.use('/api/investment_ledger', (req, res, next) => {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate')
res.set('Pragma', 'no-cache')
@@ -257,6 +262,13 @@ const bindRoutes = app => {
next()
})
app.use('/api/investment_ledger', (req, res, next) => {
if (typeof options.requireAuth === 'function') {
return options.requireAuth(req, res, next)
}
return next()
})
app.get('/api/investment_ledger/ping', (req, res) => {
res.json({ ok: true, ts: Date.now() })
})
@@ -265,24 +277,20 @@ const bindRoutes = app => {
try {
const symbol = String((req.body && req.body.symbol) || '').trim()
const rawText = String((req.body && req.body.raw_text) || '')
const filePath = String((req.body && req.body.file_path) || '').trim()
if (!rawText) return sendError(res, 400, 'empty_input')
if (String((req.body && req.body.file_path) || '').trim()) {
return sendError(res, 400, 'file_path_not_allowed')
}
let text = rawText
let encoding = 'utf8'
if (!text && filePath) {
const abs = path.resolve(filePath)
if (!fs.existsSync(abs)) return res.status(400).json({ ok: false, error: 'file_not_found' })
const decoded = readTextWithFallback(abs)
text = decoded.text
encoding = decoded.encoding
}
if (!text) return res.status(400).json({ ok: false, error: 'empty_input' })
const rows = parseCsvText(text)
const events = normalizeRowsToEvents(rows, symbol)
const summary = summarizeEvents(events)
logJSON('investment_ledger.parse', { symbol, encoding, events: events.length }, 'investment_ledger')
res.json({ ok: true, encoding, summary, rows: events })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
logJSON('investment_ledger.parse.error', { error: String(e.message || e) }, 'investment_ledger')
sendError(res, 500, INTERNAL_ERROR)
}
})
}
+10 -1
View File
@@ -118,6 +118,15 @@ const requireAuth = (req, res, next) => {
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()
@@ -151,7 +160,7 @@ const bindRoutes = app => {
})
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (serverToken) {
const clientToken = String(req.query.token || '')
const clientToken = getLocalCredsToken(req)
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
}
const u = creds.LBL_USERNAME || ''
+9 -1
View File
@@ -139,6 +139,14 @@ const getExpectedToken = () => {
const creds = readLocalCreds()
return String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '').trim()
}
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 signAuthPayload = (username) => {
const payload = { sub: username, iat: Date.now() }
@@ -495,7 +503,7 @@ const createRouter = () => {
router.get('/auth/local_creds', (req, res) => {
try {
const token = String((req.query && req.query.token) || '').trim()
const token = getLocalCredsToken(req)
const expectedToken = getExpectedToken()
if (!expectedToken || token !== expectedToken) return res.status(401).json({ ok: false, error: '令牌无效' })
const username = getExpectedUsername()
+9 -1
View File
@@ -320,6 +320,14 @@ const getExpectedToken = () => {
const creds = readLocalCreds()
return String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '').trim()
}
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 createRouter = () => {
const router = express.Router()
@@ -374,7 +382,7 @@ const createRouter = () => {
router.get('/auth/local_creds', (req, res) => {
try {
const token = String((req.query && req.query.token) || '').trim()
const token = getLocalCredsToken(req)
const expectedToken = getExpectedToken()
if (!expectedToken || token !== expectedToken) {
return res.status(401).json({ ok: false, error: 'invalid_token' })
+10 -1
View File
@@ -118,6 +118,15 @@ const requireAuth = (req, res, next) => {
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()
@@ -151,7 +160,7 @@ const bindRoutes = app => {
})
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (serverToken) {
const clientToken = String(req.query.token || '')
const clientToken = getLocalCredsToken(req)
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
}
const u = creds.RL_USERNAME || ''
+10 -1
View File
@@ -29,6 +29,15 @@ const sealPath = path.join(dataDir, 'weekly.enc')
initDB(dbPath, sealPath)
core.ensureDefaultUser()
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 ''
}
// 3. Helper Middleware
const upload = multer({ limits: { fileSize: 50 * 1024 * 1024 } })
@@ -145,7 +154,7 @@ const bindRoutes = (app) => {
})
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (serverToken) {
const clientToken = String(req.query.token || '')
const clientToken = getLocalCredsToken(req)
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
}
const username = creds.WEEKLY_USERNAME || ''