feat(auth): 添加认证模块和图片批注功能(项目初始化)

- 实现用户登录、登出、密码修改等认证功能
- 添加会话管理和权限控制中间件
- 创建图片批注组件和相关API路由
- 实现批注的增删改查功能
- 添加Docker和Git忽略配置文件
- 创建系统架构文档和开发约定说明
- 集成认证模块到前端应用路由中
This commit is contained in:
yuzhe
2026-07-21 15:28:55 +08:00
commit b0c498fbb6
81 changed files with 10826 additions and 0 deletions

48
api/customerAuth.ts Normal file
View File

@@ -0,0 +1,48 @@
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<string> {
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<NonNullable<CustomerRequest['customer']>>(`
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();
}