Files
AIclinicalresearch/backend/src/common/auth/jwt.service.ts
HaHafeng 96290d2f76 feat(aia): Implement Protocol Agent MVP with reusable Agent framework
Sprint 1-3 Completed (Backend + Frontend):

Backend (Sprint 1-2):
- Implement 5-layer Agent framework (Query->Planner->Executor->Tools->Reflection)
- Create agent_schema with 6 tables (agent_definitions, stages, prompts, sessions, traces, reflexion_rules)
- Create protocol_schema with 2 tables (protocol_contexts, protocol_generations)
- Implement Protocol Agent core services (Orchestrator, ContextService, PromptBuilder)
- Integrate LLM service adapter (DeepSeek/Qwen/GPT-5/Claude)
- 6 API endpoints with full authentication
- 10/10 API tests passed

Frontend (Sprint 3):
- Add Protocol Agent entry in AgentHub (indigo theme card)
- Implement ProtocolAgentPage with 3-column layout
- Collapsible sidebar (Gemini style, 48px <-> 280px)
- StatePanel with 5 stage cards (scientific_question, pico, study_design, sample_size, endpoints)
- ChatArea with sync button and action cards integration
- 100% prototype design restoration (608 lines CSS)
- Detailed endpoints structure: baseline, exposure, outcomes, confounders

Features:
- 5-stage dialogue flow for research protocol design
- Conversation-driven interaction with sync-to-protocol button
- Real-time context state management
- One-click protocol generation button (UI ready, backend pending)

Database:
- agent_schema: 6 tables for reusable Agent framework
- protocol_schema: 2 tables for Protocol Agent
- Seed data: 1 agent + 5 stages + 9 prompts + 4 reflexion rules

Code Stats:
- Backend: 13 files, 4338 lines
- Frontend: 14 files, 2071 lines
- Total: 27 files, 6409 lines

Status: MVP core functionality completed, pending frontend-backend integration testing

Next: Sprint 4 - One-click protocol generation + Word export
2026-01-24 17:29:24 +08:00

208 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* JWT Service
*
* JWT令牌的生成、验证和刷新
*
* 设计原则:
* - 使用 jsonwebtoken 库
* - Token payload 包含用户ID、角色、租户ID
* - 支持 Access Token 和 Refresh Token
*/
import jwt, { SignOptions, JwtPayload } from 'jsonwebtoken';
import { config } from '../../config/env.js';
/**
* JWT Payload 接口
*/
export interface JWTPayload {
/** 用户ID */
userId: string;
/** 用户手机号 */
phone: string;
/** 用户角色 */
role: string;
/** 租户ID */
tenantId: string;
/** 租户Code用于URL路由 */
tenantCode?: string;
/** 是否为默认密码 */
isDefaultPassword?: boolean;
}
/**
* Token 响应接口
*/
export interface TokenResponse {
accessToken: string;
refreshToken: string;
expiresIn: number; // 秒
tokenType: 'Bearer';
}
/**
* 解码后的 Token 接口
*/
export interface DecodedToken extends JWTPayload {
iat: number; // 签发时间
exp: number; // 过期时间
}
// Token 配置
const ACCESS_TOKEN_EXPIRES_IN = '2h'; // Access Token 2小时过期
const REFRESH_TOKEN_EXPIRES_IN = '7d'; // Refresh Token 7天过期
const ACCESS_TOKEN_EXPIRES_SECONDS = 2 * 60 * 60; // 7200秒
/**
* JWT Service 类
*/
export class JWTService {
private readonly secret: string;
constructor() {
this.secret = config.jwtSecret;
if (this.secret === 'your-secret-key-change-in-production' && config.nodeEnv === 'production') {
throw new Error('JWT_SECRET must be configured in production environment');
}
}
/**
* 生成 Access Token
*/
generateAccessToken(payload: JWTPayload): string {
const options: SignOptions = {
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
issuer: 'aiclinical',
subject: payload.userId,
};
return jwt.sign(payload, this.secret, options);
}
/**
* 生成 Refresh Token
*/
generateRefreshToken(payload: JWTPayload): string {
// Refresh Token 只包含必要信息
const refreshPayload = {
userId: payload.userId,
type: 'refresh',
};
const options: SignOptions = {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
issuer: 'aiclinical',
subject: payload.userId,
};
return jwt.sign(refreshPayload, this.secret, options);
}
/**
* 生成完整的 Token 响应
*/
generateTokens(payload: JWTPayload): TokenResponse {
return {
accessToken: this.generateAccessToken(payload),
refreshToken: this.generateRefreshToken(payload),
expiresIn: ACCESS_TOKEN_EXPIRES_SECONDS,
tokenType: 'Bearer',
};
}
/**
* 验证 Token
*/
verifyToken(token: string): DecodedToken {
try {
const decoded = jwt.verify(token, this.secret, {
issuer: 'aiclinical',
}) as DecodedToken;
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Token已过期');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error('Token无效');
}
throw error;
}
}
/**
* 刷新 Token
*
* 使用 Refresh Token 获取新的 Access Token
*/
async refreshToken(refreshToken: string, getUserById: (id: string) => Promise<JWTPayload | null>): Promise<TokenResponse> {
// 验证 Refresh Token
const decoded = this.verifyToken(refreshToken);
if ((decoded as any).type !== 'refresh') {
throw new Error('无效的Refresh Token');
}
// 获取用户最新信息
const user = await getUserById(decoded.userId);
if (!user) {
throw new Error('用户不存在');
}
// 生成新的 Tokens
return this.generateTokens(user);
}
/**
* 从 Authorization Header 中提取 Token
*/
extractTokenFromHeader(authHeader: string | undefined): string | null {
if (!authHeader) {
return null;
}
const [type, token] = authHeader.split(' ');
if (type !== 'Bearer' || !token) {
return null;
}
return token;
}
/**
* 解码 Token不验证签名
*
* 用于调试或获取过期token的payload
*/
decodeToken(token: string): DecodedToken | null {
const decoded = jwt.decode(token);
return decoded as DecodedToken | null;
}
}
// 导出单例
export const jwtService = new JWTService();