Files
xiaocai/server/src/routes/auth.ts
wangxiaogang 61f9b33f8c feat(iter-v2): T01 partial - TOKEN_SECRET check, SQL param, export credential, docs
- 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
2026-06-11 08:58:24 +08:00

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