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