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:
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -1119,4 +1128,4 @@
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', main)
|
||||
})()
|
||||
})()
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user