- 实现用户登录、登出、密码修改等认证功能 - 添加会话管理和权限控制中间件 - 创建图片批注组件和相关API路由 - 实现批注的增删改查功能 - 添加Docker和Git忽略配置文件 - 创建系统架构文档和开发约定说明 - 集成认证模块到前端应用路由中
115 lines
5.2 KiB
TypeScript
115 lines
5.2 KiB
TypeScript
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<string> {
|
|
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<ApiKeyPrincipal>(`
|
|
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<Omit<CurrentUser, 'must_change_password'> & { 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<boolean> {
|
|
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<string, unknown> = {}) {
|
|
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}`;
|
|
}
|