feat(token_lab): 新增令牌工坊工具及配套后端逻辑
新增完整的令牌工坊工具链: - 添加前端静态资源:登录界面、导航与Tool令牌管理页面,配套样式与脚本文件 - 添加后端接口与中间件:认证相关接口、路由权限控制,以及导航令牌的生成验证API - 添加默认配置文件,配置令牌工坊默认走导航登录,不对外公网开放
This commit is contained in:
@@ -1573,6 +1573,24 @@ const signJwtRS256 = (payload, jwk) => {
|
||||
const sig = crypto.sign('RSA-SHA256', Buffer.from(data), priv).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')
|
||||
return data + '.' + sig
|
||||
}
|
||||
const extractJwtFromInput = (raw) => {
|
||||
const text = String(raw || '').trim()
|
||||
if (!text) return ''
|
||||
if (/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(text)) return text
|
||||
try {
|
||||
const u = new URL(text)
|
||||
const fromQuery = String(u.searchParams.get('token') || '').trim()
|
||||
if (fromQuery) return fromQuery
|
||||
} catch {}
|
||||
const match = text.match(/(?:^|[?&])token=([^&#\s]+)/)
|
||||
if (match && match[1]) return decodeURIComponent(match[1])
|
||||
return ''
|
||||
}
|
||||
const appendTokenToUrl = (baseUrl, token) => {
|
||||
const base = String(baseUrl || '').trim()
|
||||
if (!base) return `/?token=${token}`
|
||||
return base.includes('?') ? `${base}&token=${token}` : `${base}?token=${token}`
|
||||
}
|
||||
const readNavTargets = () => {
|
||||
try {
|
||||
const s = String(process.env.NAV_TARGETS_JSON || '')
|
||||
@@ -4145,6 +4163,98 @@ app.get('/api/debug/nav/private', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/token_lab/auth/status', (req, res) => {
|
||||
try {
|
||||
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
return res.json({
|
||||
ok: true,
|
||||
authenticated: hasTokenLabAuth(req)
|
||||
})
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/token_lab/auth/login', (req, res) => {
|
||||
try {
|
||||
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
const username = String(req.body?.username || '').trim()
|
||||
const password = String(req.body?.password || '')
|
||||
const expectedUser = String(process.env.TOKEN_LAB_USERNAME || '').trim()
|
||||
const expectedPass = String(process.env.TOKEN_LAB_PASSWORD || '')
|
||||
if (!expectedUser || !expectedPass) return res.status(500).json({ ok: false, error: 'token_lab_creds_not_configured' })
|
||||
if (username !== expectedUser || password !== expectedPass) {
|
||||
return res.status(401).json({ ok: false, error: '账号或密码错误' })
|
||||
}
|
||||
res.cookie(TOKEN_LAB_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax', maxAge: getTokenLabAuthMaxAge(), path: '/' })
|
||||
return res.json({ ok: true })
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/token_lab/auth/logout', (req, res) => {
|
||||
try {
|
||||
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
res.clearCookie(TOKEN_LAB_AUTH_COOKIE, { httpOnly: true, sameSite: 'lax', path: '/' })
|
||||
return res.json({ ok: true })
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.use('/api/token_lab', (req, res, next) => {
|
||||
try {
|
||||
if (!hasNavAuthRoot(req)) return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
if (req.path === '/auth/status' || req.path === '/auth/login' || req.path === '/auth/logout') return next()
|
||||
if (hasTokenLabAuth(req)) return next()
|
||||
return res.status(401).json({ ok: false, error: 'token_lab_login_required' })
|
||||
} catch {
|
||||
return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/token_lab/nav/generate', (req, res) => {
|
||||
try {
|
||||
const baseUrl = String(req.body?.baseUrl || '').trim()
|
||||
const exp = parseInt(String(req.body?.exp || ''), 10)
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
if (!Number.isFinite(exp) || exp <= now) return res.status(400).json({ ok: false, error: 'bad_exp' })
|
||||
if (exp > now + 3600 * 24 * 365 * 2) return res.status(400).json({ ok: false, error: 'exp_too_far' })
|
||||
const priv = readNavPrivateJwk()
|
||||
if (!priv) return res.status(500).json({ ok: false, error: 'nav_private_key_missing' })
|
||||
const payload = {
|
||||
iss: getNavIssFromFlags(),
|
||||
iat: now,
|
||||
exp,
|
||||
jti: crypto.randomUUID()
|
||||
}
|
||||
const token = signJwtRS256(payload, priv)
|
||||
const link = appendTokenToUrl(baseUrl || '/', token)
|
||||
return res.json({ ok: true, token, link, payload })
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/token_lab/nav/verify', (req, res) => {
|
||||
try {
|
||||
const raw = String(req.body?.value || '').trim()
|
||||
const token = extractJwtFromInput(raw)
|
||||
if (!token) return res.status(400).json({ ok: false, error: 'missing_token' })
|
||||
const result = debugVerifyJwt(token, [], { issStrict: computeNavIssStrict() })
|
||||
return res.json({
|
||||
ok: true,
|
||||
token,
|
||||
result,
|
||||
payload: result.payload || null,
|
||||
header: result.header || null
|
||||
})
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/flags', (req, res) => {
|
||||
const enabled = computeDebugEnabled()
|
||||
res.json({ ok: true, debugToolEnabled: enabled, debug: { weibo: computeToolDebug('weibo'), markets: computeToolDebug('markets'), wall: computeToolDebug('wall'), funds_guoxin: computeToolDebug('funds_guoxin'), funds_huatai: computeToolDebug('funds_huatai'), weekly: computeToolDebug('weekly') } })
|
||||
@@ -5545,12 +5655,20 @@ app.get('/api/archives/get', (req, res) => {
|
||||
})
|
||||
|
||||
const NAV_AUTH_COOKIE = 'nav_gate'
|
||||
const TOKEN_LAB_AUTH_COOKIE = 'token_lab_gate'
|
||||
const hasNavAuthRoot = (req) => {
|
||||
try {
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
return cookies[NAV_AUTH_COOKIE] === '1'
|
||||
} catch { return false }
|
||||
}
|
||||
const hasTokenLabAuth = (req) => {
|
||||
try {
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
return cookies[TOKEN_LAB_AUTH_COOKIE] === '1'
|
||||
} catch { return false }
|
||||
}
|
||||
const getTokenLabAuthMaxAge = () => 1000 * 60 * 60 * 24
|
||||
|
||||
app.get('/index.html', (req, res) => {
|
||||
try {
|
||||
@@ -6195,6 +6313,35 @@ 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.get('/tools/token_lab', (req, res) => {
|
||||
try {
|
||||
if (hasNavAuthRoot(req)) return res.redirect('/tools/token_lab/index.html')
|
||||
const token = String(req.query.token || '').trim()
|
||||
if (!token) return res.status(401).send('未授权')
|
||||
const payload = verifyJwtWithKeys(token, [])
|
||||
if (!payload) return res.status(401).send('未授权')
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-token_lab')) return res.status(401).send('未授权')
|
||||
res.cookie(NAV_AUTH_COOKIE, '1', { httpOnly: true, sameSite: 'lax' })
|
||||
return res.redirect('/tools/token_lab/index.html')
|
||||
} catch {
|
||||
return res.status(401).send('未授权')
|
||||
}
|
||||
})
|
||||
app.use('/tools/token_lab', (req, res, next) => {
|
||||
try {
|
||||
if (hasNavAuthRoot(req)) return next()
|
||||
return res.status(401).send('未授权')
|
||||
} catch {
|
||||
return res.status(401).send('未授权')
|
||||
}
|
||||
})
|
||||
app.use('/tools/token_lab', (req, res, next) => {
|
||||
res.set('X-Frame-Options', 'SAMEORIGIN')
|
||||
next()
|
||||
})
|
||||
app.use('/tools/token_lab', express.static(path.join(process.cwd(), 'public', 'tools', 'token_lab')))
|
||||
app.use('/tools/investment_ledger', (req, res, next) => {
|
||||
try {
|
||||
if (hasNavAuthRoot(req)) return next()
|
||||
|
||||
Reference in New Issue
Block a user