import { randomBytes } from 'crypto'; import type { NextFunction, Request, Response } from 'express'; import { database } from './database.js'; import { sha256 } from './auth.js'; export type CustomerRequest = Request & { customer?: { project_id: number; reviewer_name: string }; }; 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 createCustomerSession(projectId: number, reviewerName: string): Promise { const token = randomBytes(32).toString('base64url'); await database.execute('INSERT INTO customer_sessions (project_id, reviewer_name, token_hash, expires_at) VALUES (?, ?, ?, ?)', [projectId, reviewerName, sha256(token), new Date(Date.now() + 7 * 86400000).toISOString()]); return token; } export function customerSessionCookie(token: string): string { const secure = process.env.NODE_ENV === 'production' ? '; Secure' : ''; return `review_session=${token}; HttpOnly; SameSite=Lax; Path=/api/review; Max-Age=604800${secure}`; } export async function optionalCustomer(req: CustomerRequest, _res: Response, next: NextFunction) { const token = readCookie(req, 'review_session'); if (token) { const row = await database.one>(` SELECT s.project_id, s.reviewer_name FROM customer_sessions s JOIN projects p ON p.id = s.project_id WHERE s.token_hash = ? AND s.expires_at > ? AND p.customer_access_enabled = TRUE AND p.status != 'archived' AND (p.access_expires_at IS NULL OR p.access_expires_at > ?) `, [sha256(token), new Date().toISOString(), new Date().toISOString()]); if (row) req.customer = row; } next(); } export async function requireCustomerProject(req: CustomerRequest, res: Response, next: NextFunction) { const project = await database.one<{ id: number }>('SELECT id FROM projects WHERE slug = ?', [req.params.slug]); if (!project || !req.customer || req.customer.project_id !== project.id) { res.status(401).json({ error: '请先输入项目访问密码' }); return; } next(); }