import { createHash, randomBytes, scryptSync, timingSafeEqual } from 'crypto'; import type { NextFunction, Request, Response } from 'express'; import type { CurrentUser, UserRole } from '../shared/types.js'; import { database } from './database.js'; export interface ApiKeyPrincipal { id: number; group_id: number | null; project_id: number | null; scope: 'platform' | 'project'; } export type AuthRequest = Request & { authUser?: CurrentUser; apiKey?: ApiKeyPrincipal }; export function sha256(value: string): string { return createHash('sha256').update(value).digest('hex'); } export function verifyPassword(password: string, stored: string): boolean { const [salt, expected] = stored.split(':'); if (!salt || !expected) return false; const actual = scryptSync(password, salt, 64); const expectedBuffer = Buffer.from(expected, 'hex'); return actual.length === expectedBuffer.length && timingSafeEqual(actual, expectedBuffer); } export function hashPassword(password: string): string { const salt = randomBytes(16).toString('hex'); return `${salt}:${scryptSync(password, salt, 64).toString('hex')}`; } export async function createSession(userId: number): Promise { const token = randomBytes(32).toString('base64url'); await database.execute('INSERT INTO sessions (user_id, token_hash, expires_at) VALUES (?, ?, ?)', [userId, sha256(token), new Date(Date.now() + 7 * 86400000).toISOString()]); return token; } function readCookie(req: Request, name: string): string | undefined { return req.headers.cookie?.split(';').map((item) => item.trim()).find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1); } export async function optionalAuth(req: AuthRequest, _res: Response, next: NextFunction) { const bearer = req.headers.authorization?.startsWith('Bearer ') ? req.headers.authorization.slice(7) : undefined; if (bearer?.startsWith('dd_')) { const key = await database.one(` SELECT k.id, k.group_id, k.project_id, k.scope FROM api_keys k LEFT JOIN operation_groups g ON g.id = k.group_id LEFT JOIN projects p ON p.id = k.project_id WHERE k.key_hash = ? AND k.status = 'active' AND (k.scope = 'platform' OR (g.status = 'active' AND p.status = 'active')) `, [sha256(bearer)]); if (key) { req.apiKey = key; await database.execute('UPDATE api_keys SET last_used_at = ? WHERE id = ?', [new Date().toISOString(), key.id]); } next(); return; } const token = bearer || readCookie(req, 'proofing_session'); if (token) { const row = await database.one & { must_change_password: boolean | number }>(` SELECT u.id, u.group_id, g.name AS group_name, u.username, u.display_name, u.role, u.must_change_password FROM sessions s JOIN users u ON u.id = s.user_id LEFT JOIN operation_groups g ON g.id = u.group_id WHERE s.token_hash = ? AND s.expires_at > ? AND u.status = 'active' `, [sha256(token), new Date().toISOString()]); if (row) req.authUser = { ...row, must_change_password: Boolean(row.must_change_password) }; } next(); } export function requireRole(...roles: UserRole[]) { return (req: AuthRequest, res: Response, next: NextFunction) => { if (!req.authUser) { res.status(401).json({ error: '请先登录运营账号' }); return; } if (!roles.includes(req.authUser.role)) { res.status(403).json({ error: '当前账号没有执行此操作的权限' }); return; } next(); }; } export function requireWriter(req: AuthRequest, res: Response, next: NextFunction) { if (req.authUser && ['platform_admin', 'group_admin', 'operator'].includes(req.authUser.role)) { next(); return; } if (req.apiKey) { next(); return; } res.status(401).json({ error: '请登录工作台账号或提供有效的 API Key' }); } export async function canWriteProject(req: AuthRequest, projectId: number): Promise { if (req.authUser?.role === 'platform_admin') return true; if (req.authUser) { const project = await database.one<{ group_id: number | null }>('SELECT group_id FROM projects WHERE id = ?', [projectId]); return Boolean(project && project.group_id === req.authUser.group_id); } if (req.apiKey?.scope === 'platform') return true; return req.apiKey?.scope === 'project' && req.apiKey.project_id === projectId; } export async function audit(req: AuthRequest, action: string, entityType: string, entityId?: number, detail: Record = {}) { await database.execute('INSERT INTO audit_logs (group_id, user_id, action, entity_type, entity_id, detail) VALUES (?, ?, ?, ?, ?, ?)', [req.authUser?.group_id ?? req.apiKey?.group_id ?? null, req.authUser?.id ?? null, action, entityType, entityId ?? null, JSON.stringify(req.apiKey ? { ...detail, apiKeyId: req.apiKey.id } : detail)]); } export async function clearSession(token: string | undefined) { if (token) await database.execute('DELETE FROM sessions WHERE token_hash = ?', [sha256(token)]); } export function sessionCookie(token: string): string { const secure = process.env.NODE_ENV === 'production' ? '; Secure' : ''; return `proofing_session=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=604800${secure}`; }