- S2: logs.ts LIMIT/OFFSET SQL parameterization (? placeholders) - S3: checkTokenSecret() startup validation, JWT_SECRET→TOKEN_SECRET in backup.ts - S3: Access Token expiry shortened to 2h (from 30d) - S4: Export download uses HMAC signed short-term token (/prepare endpoint) - S4: Frontend export.ts adapted for prepare-then-download flow - .env.example reorganized with security notes - Added PRD and architecture docs for iteration v2
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
import { Router, Request, Response } from 'express'
|
|
import { createHmac } from 'crypto'
|
|
import pool from '../db/connection'
|
|
|
|
const router = Router()
|
|
|
|
const APPID = process.env.WX_APPID || ''
|
|
const SECRET = process.env.WX_SECRET || ''
|
|
const TOKEN_SECRET = process.env.TOKEN_SECRET || 'xiaocai-token-secret-change-in-production'
|
|
|
|
/** 启动时检查 TOKEN_SECRET 是否为默认占位符,是则拒绝启动 */
|
|
export function checkTokenSecret(): void {
|
|
const defaults = [
|
|
'xiaocai-token-secret-change-in-production',
|
|
'change-me-to-a-secure-random-string',
|
|
]
|
|
if (defaults.includes(TOKEN_SECRET)) {
|
|
console.error('[Auth] FATAL: TOKEN_SECRET is using a default placeholder. Set a secure TOKEN_SECRET in .env before starting the server.')
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
export function signToken(userId: number): string {
|
|
const payload = `${userId}:${Date.now()}`
|
|
const signature = createHmac('sha256', TOKEN_SECRET).update(payload).digest('hex')
|
|
return Buffer.from(`${payload}:${signature}`).toString('base64')
|
|
}
|
|
|
|
// H5 演示登录 — 创建/复用演示用户
|
|
router.post('/demo-login', async (_req: Request, res: Response) => {
|
|
try {
|
|
const demoOpenid = 'h5_demo_user'
|
|
const [result] = await pool.query(
|
|
'INSERT INTO users (openid, session_key) VALUES (?, ?) ON DUPLICATE KEY UPDATE session_key = ?, id = LAST_INSERT_ID(id)',
|
|
[demoOpenid, 'demo', 'demo']
|
|
)
|
|
const userId = (result as any).insertId
|
|
|
|
// 首个用户自动设为管理员
|
|
await autoSetAdmin(userId)
|
|
|
|
const token = signToken(userId)
|
|
const [userRows] = await pool.query('SELECT nickname, avatar_url, role FROM users WHERE id = ?', [userId])
|
|
const userInfo = (userRows as any[])[0]
|
|
res.json({ code: 0, data: { token, userId, nickname: userInfo?.nickname || '小菜', avatar_url: userInfo?.avatar_url || '', role: userInfo?.role || 'user' } })
|
|
} catch (err: any) {
|
|
console.error('[Auth] Demo login failed:', err)
|
|
res.status(500).json({ code: 50000, message: '服务器错误' })
|
|
}
|
|
})
|
|
|
|
router.post('/login', async (req: Request, res: Response) => {
|
|
try {
|
|
const { code } = req.body
|
|
if (!code || typeof code !== 'string' || code.length > 128 || !/^[a-zA-Z0-9_-]+$/.test(code)) {
|
|
return res.status(400).json({ code: 40001, message: 'code参数无效' })
|
|
}
|
|
|
|
// 调用微信 code2session 接口
|
|
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
|
|
const wxRes = await fetch(url)
|
|
const wxData = await wxRes.json() as any
|
|
|
|
if (wxData.errcode) {
|
|
console.error('[Auth] WeChat API error:', wxData.errcode, wxData.errmsg)
|
|
return res.status(400).json({ code: 40002, message: '微信登录失败' })
|
|
}
|
|
|
|
const { openid, session_key } = wxData
|
|
|
|
// 查找或创建用户 (atomic: handles race condition)
|
|
const [result] = await pool.query(
|
|
'INSERT INTO users (openid, session_key) VALUES (?, ?) ON DUPLICATE KEY UPDATE session_key = ?, id = LAST_INSERT_ID(id)',
|
|
[openid, session_key, session_key]
|
|
)
|
|
const userId = (result as any).insertId
|
|
|
|
// 首个用户自动设为管理员
|
|
await autoSetAdmin(userId)
|
|
|
|
// HMAC signed token with expiry
|
|
const token = signToken(userId)
|
|
const [userRows] = await pool.query('SELECT nickname, avatar_url, role FROM users WHERE id = ?', [userId])
|
|
const userInfo = (userRows as any[])[0]
|
|
res.json({ code: 0, data: { token, userId, nickname: userInfo?.nickname || '小菜', avatar_url: userInfo?.avatar_url || '', role: userInfo?.role || 'user' } })
|
|
} catch (err: any) {
|
|
console.error('[Auth] Login failed:', err)
|
|
res.status(500).json({ code: 50000, message: '服务器错误' })
|
|
}
|
|
})
|
|
|
|
/** 首个用户自动设为管理员 */
|
|
async function autoSetAdmin(userId: number) {
|
|
const [adminCount] = await pool.query('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin'])
|
|
if ((adminCount as any[])[0].count === 0) {
|
|
await pool.query('UPDATE users SET role = ? WHERE id = ?', ['admin', userId])
|
|
console.log(`[Auth] User ${userId} auto-promoted to admin`)
|
|
}
|
|
}
|
|
|
|
export default router
|