From 7a3b9a369496cfc54653d221c963f3030e008d79 Mon Sep 17 00:00:00 2001 From: empty Date: Tue, 3 Feb 2026 21:35:04 +0800 Subject: [PATCH] chore: add missing auth utils and public routes Co-Authored-By: Claude Opus 4.5 --- packages/server/src/routes/public.routes.ts | 45 +++++++++++ packages/server/src/utils/auth.ts | 83 +++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 packages/server/src/routes/public.routes.ts create mode 100644 packages/server/src/utils/auth.ts diff --git a/packages/server/src/routes/public.routes.ts b/packages/server/src/routes/public.routes.ts new file mode 100644 index 0000000..ea2ddfa --- /dev/null +++ b/packages/server/src/routes/public.routes.ts @@ -0,0 +1,45 @@ +import { Router, IRouter } from 'express'; +import { prizeConfigService } from '../services/prize-config.service'; +import { participantService } from '../services/participant.service'; + +const router: IRouter = Router(); + +/** + * GET /api/public/prizes + * Public read-only prize configuration (for screen display) + */ +router.get('/prizes', (_req, res, next) => { + try { + const config = prizeConfigService.getFullConfig(); + return res.json({ + success: true, + data: config, + }); + } catch (error) { + next(error); + } +}); + +/** + * GET /api/public/participants + * Public read-only participant list (for screen display) + */ +router.get('/participants', (_req, res, next) => { + try { + const participants = participantService.getAll(); + const stats = participantService.getStats(); + return res.json({ + success: true, + data: { + count: participants.length, + tagDistribution: stats.tagDistribution, + participants, + }, + }); + } catch (error) { + next(error); + } +}); + +export default router; + diff --git a/packages/server/src/utils/auth.ts b/packages/server/src/utils/auth.ts new file mode 100644 index 0000000..bbde0bc --- /dev/null +++ b/packages/server/src/utils/auth.ts @@ -0,0 +1,83 @@ +import jwt from 'jsonwebtoken'; +import { redis } from '../config/redis'; +import { config } from '../config'; + +export type AuthRole = 'user' | 'admin' | 'screen'; + +export interface SessionData { + userId: string; + userName?: string; + role?: AuthRole; + openid?: string; +} + +export interface AuthUser { + userId: string; + userName?: string; + role: AuthRole; + token: string; + openid?: string; +} + +const SESSION_TOKEN_PREFIX = 'session:'; + +export function extractBearerToken(raw?: string | string[]): string | null { + if (!raw) return null; + const value = Array.isArray(raw) ? raw[0] : raw; + const parts = value.split(' '); + if (parts.length === 2 && /^Bearer$/i.test(parts[0])) { + return parts[1]; + } + return null; +} + +export async function createSessionToken( + data: SessionData, + ttlSeconds: number +): Promise { + const payload = { + userId: data.userId, + userName: data.userName, + role: data.role || 'user', + openid: data.openid, + }; + + const token = jwt.sign(payload, config.jwtSecret, { + expiresIn: ttlSeconds, + }); + + await redis.setex( + `${SESSION_TOKEN_PREFIX}${token}`, + ttlSeconds, + JSON.stringify({ + userId: data.userId, + userName: data.userName, + role: data.role || 'user', + openid: data.openid, + }) + ); + + return token; +} + +export async function verifySessionToken(token: string): Promise { + try { + const payload = jwt.verify(token, config.jwtSecret) as SessionData & { role?: AuthRole }; + const data = await redis.get(`${SESSION_TOKEN_PREFIX}${token}`); + if (!data) return null; + + const session = JSON.parse(data) as SessionData; + const role = (payload.role || session.role || 'user') as AuthRole; + + return { + userId: payload.userId || session.userId, + userName: payload.userName || session.userName, + role, + token, + openid: payload.openid || session.openid, + }; + } catch { + return null; + } +} +