diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..997eacd --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# APP ID +NEXT_PUBLIC_APP_ID=http://your-dify-host/app/your-app-id/configuration + +# APP API key +NEXT_PUBLIC_APP_KEY=app-your-api-key + +# Dify API URL +NEXT_PUBLIC_API_URL=http://your-dify-api-url + +# JWT Secret - 用于签名和验证前端JWT token +# ⚠️ 生产环境请务必修改为强随机字符串(建议至少64个字符) +# 可以使用以下命令生成: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production diff --git a/app/api/auth/user-routes.ts b/app/api/auth/user-routes.ts index 065c00d..96e4a25 100644 --- a/app/api/auth/user-routes.ts +++ b/app/api/auth/user-routes.ts @@ -462,7 +462,7 @@ const FALLBACK_MENU_DATA: Record = { * @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader') * @returns 用户可访问的路由列表 */ -export async function getUserRoutesByRole(roleKey: string): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> { +export async function getUserRoutesByRole(roleKey: string, jwt?: string): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> { try { console.log(`获取角色 ${roleKey} 的路由权限`); @@ -470,7 +470,8 @@ export async function getUserRoutesByRole(roleKey: string): Promise<{ success: b const roleResult = await postgrestGet>("roles", { filter: { "role_key": `eq.${roleKey}` - } + }, + token: jwt }); if (roleResult.error || !roleResult.data || roleResult.data.length === 0) { @@ -485,7 +486,8 @@ export async function getUserRoutesByRole(roleKey: string): Promise<{ success: b const roleRoutesResult = await postgrestGet>("role_route", { filter: { "role_id": `eq.${roleId}` - } + }, + token: jwt }); if (roleRoutesResult.error) { @@ -509,7 +511,8 @@ export async function getUserRoutesByRole(roleKey: string): Promise<{ success: b "id": `in.(${routeIds.join(',')})`, "is_menu": "eq.1" }, - order: "parent_id,meta->>order" + order: "parent_id,meta->>order", + token: jwt }); if (routesResult.error) { diff --git a/app/api/axios-client.ts b/app/api/axios-client.ts index bf8de24..99dbff4 100644 --- a/app/api/axios-client.ts +++ b/app/api/axios-client.ts @@ -386,15 +386,18 @@ export async function del(endpoint: string, params?: QueryParams): Promise { - const downloadUrl = `${DOCUMENT_URL}${path}`; - + // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 + const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`; + try { // console.log(`📦 axios-client.ts->下载文件: ${downloadUrl}`); - const response = await axios.get(downloadUrl, { - responseType: 'blob' - }); - - return response.data; + const response = await fetch(downloadUrl); + + if (!response.ok) { + throw new Error(`下载失败: ${response.status} ${response.statusText}`); + } + + return await response.blob(); } catch (error) { console.error('下载文件失败:', error); throw error; diff --git a/app/api/contract-template/templates.ts b/app/api/contract-template/templates.ts index 439724a..8ffe97d 100644 --- a/app/api/contract-template/templates.ts +++ b/app/api/contract-template/templates.ts @@ -58,6 +58,7 @@ export interface TemplateSearchParams { pageSize?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; + token?: string; // JWT token } export interface SearchResult { @@ -70,12 +71,14 @@ export interface SearchResult { /** * 获取所有合同分类 + * @param jwt JWT token (可选) */ -export async function getContractCategories() { +export async function getContractCategories(jwt?: string) { try { const params: PostgrestParams = { select: '*', - order: 'sort_order.asc,name.asc' + order: 'sort_order.asc,name.asc', + token: jwt }; const response = await postgrestGet('contract_categories', params); @@ -98,13 +101,15 @@ export async function getContractCategories() { /** * 获取所有合同分类及其模板数量(使用聚合查询) + * @param jwt JWT token (可选) */ -export async function getContractCategoriesWithCount() { +export async function getContractCategoriesWithCount(jwt?: string) { try { // 获取所有分类 const categoriesResponse = await postgrestGet('contract_categories', { select: '*', - order: 'sort_order.asc,name.asc' + order: 'sort_order.asc,name.asc', + token: jwt }); if (categoriesResponse.error) { @@ -120,7 +125,8 @@ export async function getContractCategoriesWithCount() { // 简化方案:获取该分类下的所有模板ID,然后计算数量 const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', { select: 'id', - filter: { 'category_id': `eq.${category.id}` } + filter: { 'category_id': `eq.${category.id}` }, + token: jwt }); let templateCount = 0; @@ -172,7 +178,8 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = page = 1, pageSize = 6, sortBy = 'updated_at', - sortOrder = 'desc' + sortOrder = 'desc', + token } = searchParams; // 构建查询参数 @@ -180,7 +187,8 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = select: 'id,template_code,title,category_id,description,file_path,file_format,is_featured,created_at,updated_at,pdf_file_path,category:contract_categories(id,name,icon,description)', limit: pageSize, offset: (page - 1) * pageSize, - order: `${sortBy}.${sortOrder}` + order: `${sortBy}.${sortOrder}`, + token }; // 构建过滤条件 @@ -207,7 +215,8 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = try { const categoryResponse = await postgrestGet('contract_categories', { select: 'id', - filter: { 'name': `ilike.*${cleanKeyword}*` } + filter: { 'name': `ilike.*${cleanKeyword}*` }, + token }); if (categoryResponse.data) { @@ -237,7 +246,8 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = if (category && !category_id) { const categoryResponse = await postgrestGet('contract_categories', { select: 'id', - filter: { 'name': `eq.${category}` } + filter: { 'name': `eq.${category}` }, + token }); if (categoryResponse.data) { @@ -265,7 +275,8 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = const countParams: PostgrestParams = { select: 'id', filter: params.filter, - or: params.or + or: params.or, + token }; const countResponse = await postgrestGet<{ id: number }[]>('contract_templates', countParams); @@ -295,12 +306,15 @@ export async function getContractTemplates(searchParams: TemplateSearchParams = /** * 根据ID获取单个合同模板 + * @param id 模板ID + * @param jwt JWT token (可选) */ -export async function getContractTemplate(id: string | number) { +export async function getContractTemplate(id: string | number, jwt?: string) { try { const params: PostgrestParams = { select: 'id,template_code,title,category_id,description,file_path,file_format,is_featured,created_at,updated_at,pdf_file_path,category:contract_categories(id,name,icon,description)', - filter: { 'id': `eq.${id}` } + filter: { 'id': `eq.${id}` }, + token: jwt }; const response = await postgrestGet('contract_templates', params); @@ -327,14 +341,17 @@ export async function getContractTemplate(id: string | number) { /** * 获取推荐模板 + * @param limit 数量限制 + * @param jwt JWT token (可选) */ -export async function getFeaturedTemplates(limit: number = 6) { +export async function getFeaturedTemplates(limit: number = 6, jwt?: string) { try { const params: PostgrestParams = { select: 'id,template_code,title,category_id,description,file_path,file_format,is_featured,created_at,updated_at,pdf_file_path,category:contract_categories(id,name,icon,description)', filter: { 'is_featured': 'eq.true' }, order: 'updated_at.desc', - limit + limit, + token: jwt }; const response = await postgrestGet('contract_templates', params); @@ -357,6 +374,8 @@ export async function getFeaturedTemplates(limit: number = 6) { /** * 搜索合同模板(智能搜索) + * @param query 搜索关键词 + * @param filters 过滤条件 */ export async function searchContractTemplates( query: string, diff --git a/app/api/cross-checking/cross-file-result.ts b/app/api/cross-checking/cross-file-result.ts index a9b3d92..bc8a7e6 100644 --- a/app/api/cross-checking/cross-file-result.ts +++ b/app/api/cross-checking/cross-file-result.ts @@ -87,13 +87,14 @@ async function safeGetJWT(jwtToken?: string): Promise { * @param userId 用户ID * @returns 是否是发起人 */ -export async function findIsProposer(taskId: string | number, userId: number | undefined): Promise { +export async function findIsProposer(taskId: string | number, userId: number | undefined, frontendJWT?: string): Promise { // 通过postgrest的get请求去cross_examination_tasks表中进行查找assignee_id是否等于userId const response = await postgrestGet(`cross_examination_tasks`, { select: 'assigner_id', filter: { id: `eq.${taskId}` - } + }, + token: frontendJWT }); if (response.error) { console.error('获取任务数据失败:', response.error); @@ -366,7 +367,8 @@ export async function performOpinionAction( * @returns 完成评查结果 */ export async function confirmReviewResults( - documentId: string | number + documentId: string | number, + frontendJWT?: string ): Promise<{data?: unknown, error?: string, status?: number}> { try { // 通过postgrest的post请求去documents表中进行查找id等于documentId的数据,更新documents表的audit_status为1 @@ -374,7 +376,7 @@ export async function confirmReviewResults( audit_status: 1 }, { id: documentId - }); + }, frontendJWT); if(response.error) { return { error: response.error, diff --git a/app/api/cross-checking/cross-files.ts b/app/api/cross-checking/cross-files.ts index 8218c50..3b5a9ce 100644 --- a/app/api/cross-checking/cross-files.ts +++ b/app/api/cross-checking/cross-files.ts @@ -482,7 +482,7 @@ export async function getTaskDocuments(taskId: number, page: number = 1, pageSiz * @param auditStatus 审核状态 * @returns 更新结果 */ -export async function updateDocumentAuditStatus(id: string, auditStatus: number): Promise<{ +export async function updateDocumentAuditStatus(id: string, auditStatus: number, frontendJWT?: string): Promise<{ success?: boolean; error?: string; status?: number; @@ -491,13 +491,14 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number) if (!id) { return { error: '文件ID不能为空', status: 400 }; } - + const response = await postgrestPut>( 'documents', { audit_status: auditStatus }, - { + { id: parseInt(id) - } + }, + frontendJWT ); if (response.error) { diff --git a/app/api/db-client.server.ts b/app/api/db-client.server.ts new file mode 100644 index 0000000..140b887 --- /dev/null +++ b/app/api/db-client.server.ts @@ -0,0 +1,111 @@ +// app/api/db-client.server.ts +import { getUserSession } from '~/api/login/auth.server'; +import { runWithContext } from './postgrest-client'; + +/** + * 在认证上下文中运行 + * + * 所有在此上下文中调用的 postgrest 函数(postgrestGet/Post/Put/Delete) + * 都会自动获取并添加 JWT Token 到 Authorization 头部 + * + * @param request Remix Request 对象 + * @param fn 要在认证上下文中执行的函数 + * @returns 函数执行结果 + * @throws 如果用户未登录则抛出错误 + * + * @example + * ```typescript + * export async function loader({ request }: LoaderFunctionArgs) { + * return await runInAuthContext(request, async () => { + * // 所有 postgrest 调用自动带 JWT + * const users = await postgrestGet('users', { limit: 10 }); + * const docs = await postgrestGet('documents', { filter: { status: 'eq.0' } }); + * + * return json({ users: users.data, docs: docs.data }); + * }); + * } + * ``` + */ +export async function runInAuthContext( + request: Request, + fn: () => T | Promise +): Promise { + const { frontendJWT, isAuthenticated } = await getUserSession(request); + + if (!isAuthenticated || !frontendJWT) { + throw new Error('用户未登录,无法执行需要认证的操作'); + } + + // 在上下文中设置 JWT,所有 postgrest 调用都会自动使用 + return runWithContext({ jwt: frontendJWT }, fn); +} + +/** + * 在公开上下文中运行(不需要认证) + * + * 用于公开数据访问,不会添加 JWT Token + * + * @param fn 要执行的函数 + * @returns 函数执行结果 + * + * @example + * ```typescript + * export async function loader() { + * return runInPublicContext(async () => { + * const articles = await postgrestGet('public_articles', { + * filter: { published: 'eq.true' } + * }); + * return json({ articles: articles.data }); + * }); + * } + * ``` + */ +export function runInPublicContext( + fn: () => T | Promise +): T | Promise { + // 在空上下文中运行,不设置 JWT + return runWithContext({}, fn); +} + +/** + * 在可选认证上下文中运行 + * + * 如果用户已登录,会自动添加 JWT; + * 如果用户未登录,则不添加 JWT(不会抛出错误) + * + * @param request Remix Request 对象 + * @param fn 要执行的函数 + * @returns 函数执行结果 + * + * @example + * ```typescript + * export async function loader({ request }: LoaderFunctionArgs) { + * return await runInOptionalAuthContext(request, async () => { + * // 如果用户登录,会带 JWT(可能看到更多内容) + * // 如果用户未登录,不带 JWT(看到基础内容) + * const content = await postgrestGet('content', { limit: 20 }); + * return json({ content: content.data }); + * }); + * } + * ``` + */ +export async function runInOptionalAuthContext( + request: Request, + fn: () => T | Promise +): Promise { + try { + const { frontendJWT, isAuthenticated } = await getUserSession(request); + + if (isAuthenticated && frontendJWT) { + // 用户已登录,使用 JWT + return runWithContext({ jwt: frontendJWT }, fn); + } + } catch (error) { + // 获取会话失败,继续以公开方式运行 + console.warn('获取用户会话失败,以公开方式运行:', error); + } + + // 用户未登录或获取会话失败,以公开方式运行 + return runWithContext({}, fn); +} + diff --git a/app/api/document-types/document-types.ts b/app/api/document-types/document-types.ts index dddf68c..f59fa20 100644 --- a/app/api/document-types/document-types.ts +++ b/app/api/document-types/document-types.ts @@ -90,18 +90,20 @@ function extractApiData(responseData: unknown): T | null { /** * 获取所有评查点分组 + * @param token JWT token (可选) * @returns 评查点分组列表 */ -export async function getAllEvaluationPointGroups(): Promise<{ +export async function getAllEvaluationPointGroups(token?: string): Promise<{ data?: DocumentTypeGroup[]; error?: string; status?: number; }> { try { const params: PostgrestParams = { - select: 'id, name' + select: 'id, name', + token }; - + const response = await postgrestGet + filter: {} as Record, + token: frontendJWT }; // 添加筛选条件 @@ -299,10 +304,10 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams = } // console.log(`文档类型 ${type.id} 的分组IDs:`, groupIds); - + // 获取这些ID对应的分组信息 - const groupsResponse = await getEvaluationPointGroupsByIds(groupIds); - + const groupsResponse = await getEvaluationPointGroupsByIds(groupIds, frontendJWT); + // 返回包含分组信息的文档类型 return { ...type, @@ -347,9 +352,10 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams = /** * 删除文档类型 * @param id 文档类型ID + * @param frontendJWT JWT token (可选) * @returns 删除结果 */ -export async function deleteDocumentType(id: string): Promise<{ +export async function deleteDocumentType(id: string, frontendJWT?: string): Promise<{ success?: boolean; error?: string; status?: number; @@ -365,7 +371,8 @@ export async function deleteDocumentType(id: string): Promise<{ { filter: { 'id': `eq.${id}` - } + }, + token: frontendJWT } ); @@ -435,9 +442,10 @@ function convertToUIDocumentType(type: DocumentType & { groups: DocumentTypeGrou /** * 获取文档类型详情 * @param id 文档类型ID + * @param frontendJWT JWT token (可选) * @returns 文档类型详情 */ -export async function getDocumentType(id: string): Promise<{ +export async function getDocumentType(id: string, frontendJWT?: string): Promise<{ data?: DocumentTypeUI; error?: string; status?: number; @@ -446,7 +454,7 @@ export async function getDocumentType(id: string): Promise<{ if (!id) { return { error: '文档类型ID不能为空', status: 400 }; } - + const params: PostgrestParams = { select: ` id, @@ -460,9 +468,10 @@ export async function getDocumentType(id: string): Promise<{ `, filter: { 'id': `eq.${id}` - } + }, + token: frontendJWT }; - + const response = await postgrestGet('document_types', params); if (response.error) { @@ -499,9 +508,9 @@ export async function getDocumentType(id: string): Promise<{ } // console.log(`文档类型 ${id} 的分组IDs:`, groupIds); - - const groupsResponse = await getEvaluationPointGroupsByIds(groupIds); - + + const groupsResponse = await getEvaluationPointGroupsByIds(groupIds, frontendJWT); + if (groupsResponse.error) { return { error: groupsResponse.error, status: 500 }; } @@ -527,7 +536,7 @@ export async function getDocumentType(id: string): Promise<{ * @param documentType 文档类型数据 * @returns 创建结果 */ -export async function createDocumentType(documentType: DocumentTypeCreateDTO): Promise<{ +export async function createDocumentType(documentType: DocumentTypeCreateDTO, frontendJWT?: string): Promise<{ data?: DocumentTypeUI; error?: string; status?: number; @@ -610,7 +619,8 @@ export async function createDocumentType(documentType: DocumentTypeCreateDTO): P // 发送创建请求 const response = await postgrestPost( 'document_types', - apiDocumentType + apiDocumentType, + frontendJWT ); if (response.error) { @@ -628,14 +638,14 @@ export async function createDocumentType(documentType: DocumentTypeCreateDTO): P } // 获取关联分组信息 - const groupsResponse = await getEvaluationPointGroupsByIds(groupIds); - + const groupsResponse = await getEvaluationPointGroupsByIds(groupIds, frontendJWT); + // 添加分组信息并转换为UI类型 const typeWithGroups = { ...newDocumentType, groups: groupsResponse.data || [] }; - + return { data: convertToUIDocumentType(typeWithGroups) }; } catch (error) { console.error('创建文档类型失败:', error); @@ -652,7 +662,7 @@ export async function createDocumentType(documentType: DocumentTypeCreateDTO): P * @param documentType 文档类型数据 * @returns 更新结果 */ -export async function updateDocumentType(id: string, documentType: DocumentTypeUpdateDTO): Promise<{ +export async function updateDocumentType(id: string, documentType: DocumentTypeUpdateDTO, frontendJWT?: string): Promise<{ data?: DocumentTypeUI; error?: string; status?: number; @@ -730,7 +740,8 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU const response = await postgrestPut( 'document_types', apiDocumentType, - {id} + {id}, + frontendJWT ); if (response.error) { @@ -748,14 +759,14 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU } // 获取关联分组信息 - const groupsResponse = await getEvaluationPointGroupsByIds(groupIds); - + const groupsResponse = await getEvaluationPointGroupsByIds(groupIds, frontendJWT); + // 添加分组信息并转换为UI类型 const typeWithGroups = { ...updatedDocumentType, groups: groupsResponse.data || [] }; - + return { data: convertToUIDocumentType(typeWithGroups) }; } catch (error) { console.error('更新文档类型失败:', error); diff --git a/app/api/evaluation_points/reviews.ts b/app/api/evaluation_points/reviews.ts index 1a8cf0e..a247cfc 100644 --- a/app/api/evaluation_points/reviews.ts +++ b/app/api/evaluation_points/reviews.ts @@ -129,9 +129,9 @@ interface ScoringProposal { * @param request Remix请求对象,用于获取用户会话 * @returns 评查点结果列表和统计数据 */ -export async function getReviewPoints(fileId: string, request: Request) { +export async function getReviewPoints(fileId: string, request: Request) { // 获取用户会话信息 - const { userInfo } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { console.error("用户身份验证失败"); @@ -141,7 +141,7 @@ export async function getReviewPoints(fileId: string, request: Request) { // const userId = userInfo.user_id.toString(); // 首先先获取这个文档的数据 - const documentData = await getDocumentWithNoUserId(fileId); + const documentData = await getDocumentWithNoUserId(fileId, frontendJWT); if (documentData.error) { console.error("获取文档数据错误:", documentData.error); return Response.json({ error: documentData.error }, { status: documentData.status || 500 }); @@ -154,7 +154,8 @@ export async function getReviewPoints(fileId: string, request: Request) { 'document_id': `eq.${fileId}` }, order: 'id.desc', - limit: 1 + limit: 1, + token: frontendJWT }; const contractStructureComparisonResponse = await postgrestGet('contract_structure_comparison', contractStructureComparisonParams); @@ -195,7 +196,8 @@ export async function getReviewPoints(fileId: string, request: Request) { select: '*', filter: { 'document_id': `eq.${fileId}` - } + }, + token: frontendJWT }; const evaluationResultsResponse = await postgrestGet('evaluation_results', evaluationResultsParams); @@ -223,7 +225,8 @@ export async function getReviewPoints(fileId: string, request: Request) { select: '*', filter: { 'id': `in.(${evaluationPointIds.join(',')})` - } + }, + token: frontendJWT }; const evaluationPointsResponse = await postgrestGet('evaluation_points', evaluationPointsParams); @@ -249,7 +252,8 @@ export async function getReviewPoints(fileId: string, request: Request) { select: '*', filter: { 'id': `in.(${groupIds.join(',')})` - } + }, + token: frontendJWT }; const groupsResponse = await postgrestGet('evaluation_point_groups', groupsParams); @@ -272,7 +276,8 @@ export async function getReviewPoints(fileId: string, request: Request) { filter: { 'document_id': `eq.${fileId}`, 'evaluation_point_id': `in.(${manualReviewPointsIds.join(',')})` - } + }, + token: frontendJWT }; const manualReviewPointsResponse = await postgrestGet('audit_status', manualReviewPointsParams); if (manualReviewPointsResponse.error) { @@ -326,7 +331,8 @@ export async function getReviewPoints(fileId: string, request: Request) { filter: { 'document_id': `eq.${fileId}`, 'deleted_at': `is.null` - } + }, + token: frontendJWT }; const scoringProposalsResponse = await postgrestGet('cross_scoring_proposals', scoringProposalsParams); @@ -754,7 +760,7 @@ export async function updateReviewResult( }> { try { // 获取用户会话信息 - const { userInfo } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { console.error("用户身份验证失败"); @@ -770,7 +776,8 @@ export async function updateReviewResult( // 首先获取当前评查结果数据 const currentResultResponse = await postgrestGet('evaluation_results', { select: '*', - filter: { id: `eq.${resultId}` } + filter: { id: `eq.${resultId}` }, + token: frontendJWT }); if (currentResultResponse.error) { @@ -805,7 +812,8 @@ export async function updateReviewResult( const resultResponse = await postgrestPut( 'evaluation_results', updatedData, - { id: resultId } + { id: resultId }, + frontendJWT ); if (resultResponse.error) { @@ -830,7 +838,8 @@ export async function updateReviewResult( { id: editAuditStatusId, user_id: userId // 添加用户ID条件,确保只能更新自己的记录 - } + }, + frontendJWT ); if (auditStatusResponse.error) { @@ -853,7 +862,7 @@ export async function updateReviewResult( }; // 使用postgrestPost创建新记录 - const postResponse = await postgrestPost('audit_status', newAuditStatus); + const postResponse = await postgrestPost('audit_status', newAuditStatus, frontendJWT); if (postResponse.error) { return { error: postResponse.error, status: postResponse.status || 500 }; @@ -889,7 +898,7 @@ export async function confirmReviewResults(documentId: string, request: Request) }> { try { // 获取用户会话信息 - const { userInfo } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { console.error("用户身份验证失败"); @@ -932,7 +941,8 @@ export async function confirmReviewResults(documentId: string, request: Request) { id: documentId, user_id: userId // 添加用户ID条件,确保只能更新自己的文档 - } + }, + frontendJWT ); if (response.error) { diff --git a/app/api/evaluation_points/rule-groups.ts b/app/api/evaluation_points/rule-groups.ts index 1538cf7..a1d872a 100644 --- a/app/api/evaluation_points/rule-groups.ts +++ b/app/api/evaluation_points/rule-groups.ts @@ -68,9 +68,10 @@ function extractApiData(responseData: unknown): T | null { /** * 获取评查点分组列表 + * @param token JWT token (可选) * @returns 评查点分组列表 */ -export async function getRuleGroups(): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getRuleGroups(token?: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { try { const params: PostgrestParams = { select: ` @@ -84,7 +85,8 @@ export async function getRuleGroups(): Promise<{data: RuleGroup[]; error?: never `, filter: { 'pid': 'eq.0' - } + }, + token }; const response = await postgrestGet<{code: number; msg: string; data: Array<{ @@ -138,9 +140,10 @@ export async function getRuleGroups(): Promise<{data: RuleGroup[]; error?: never /** * 获取指定分组的子分组 * @param parentId 父分组ID + * @param token JWT token (可选) * @returns 子分组列表 */ -export async function getChildGroups(parentId: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getChildGroups(parentId: string, token?: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { try { // 1. 获取子分组 const childGroupsParams: PostgrestParams = { @@ -154,7 +157,8 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup `, filter: { 'pid': `eq.${parentId}` - } + }, + token }; const childGroupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{ @@ -179,7 +183,8 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup select: 'id', filter: { 'evaluation_point_groups_id': `eq.${group.id}` - } + }, + token }; const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); @@ -203,7 +208,8 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup select: 'id', filter: { 'evaluation_point_groups_id': `eq.${group.id}` - } + }, + token }; const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); @@ -234,9 +240,10 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup /** * 获取所有评查点分组(包括一级和二级) + * @param token JWT token (可选) * @returns 完整的评查点分组列表 */ -export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getAllRuleGroups(token?: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { try { // 1. 获取所有分组 const allGroupsParams: PostgrestParams = { @@ -245,7 +252,8 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne pid, name, is_enabled - ` + `, + token }; const allGroupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{ @@ -292,7 +300,8 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne select: 'id', filter: { 'evaluation_point_groups_id': `eq.${child.id}` - } + }, + token }; const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); @@ -316,9 +325,10 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne /** * 获取单个评查点分组详情 * @param id 分组ID + * @param token JWT token (可选) * @returns 分组详情 */ -export async function getRuleGroup(id: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { +export async function getRuleGroup(id: string, token?: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { try { if (!id) { return { error: '分组ID不能为空', status: 400 }; @@ -336,7 +346,8 @@ export async function getRuleGroup(id: string): Promise<{data: RuleGroup; error? `, filter: { 'id': `eq.${id}` - } + }, + token }; const response = await postgrestGet<{code: number; msg: string; data: Array<{ @@ -389,7 +400,8 @@ export async function getRuleGroup(id: string): Promise<{data: RuleGroup; error? select: 'id', filter: { 'evaluation_point_groups_id': `eq.${group.id}` - } + }, + token }; const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); @@ -412,9 +424,10 @@ export async function getRuleGroup(id: string): Promise<{data: RuleGroup; error? /** * 创建评查点分组 * @param groupData 分组数据 + * @param token JWT token (可选) * @returns 创建的分组 */ -export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { +export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token?: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { try { // 验证必填字段 if (!groupData.name || !groupData.code) { @@ -447,7 +460,8 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto): Prom // 直接发送到 PostgreSQL 表 const response = await postgrestPost | ApiRuleGroup, ApiRuleGroup>( 'evaluation_point_groups', // 表名 - apiGroup + apiGroup, + token ); if (response.error) { @@ -490,15 +504,17 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto): Prom * 更新评查点分组 * @param id 分组ID * @param data 更新的分组数据 + * @param token JWT token (可选) * @returns 更新后的分组 */ -export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { +export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto, token?: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { try { // 使用新的filters参数 const response = await postgrestPut | RuleGroup, RuleGroupCreateUpdateDto>( 'evaluation_point_groups', data, - { id } + { id }, + token ); if (response.error) { @@ -524,12 +540,13 @@ export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto /** * 删除评查点分组 * @param id 分组ID + * @param token JWT token (可选) * @returns 删除结果 */ -export async function deleteRuleGroup(id: string): Promise<{success: boolean; error?: string}> { +export async function deleteRuleGroup(id: string, token?: string): Promise<{success: boolean; error?: string}> { try { // 1. 首先获取分组信息,判断是一级还是二级分组 - const groupResponse = await getRuleGroup(id); + const groupResponse = await getRuleGroup(id, token); if (groupResponse.error) { return { success: false, error: groupResponse.error }; } @@ -542,7 +559,7 @@ export async function deleteRuleGroup(id: string): Promise<{success: boolean; er // 2. 如果是一级分组,需要先删除所有子分组 if (group.pid === '0') { // 获取所有子分组 - const childGroupsResponse = await getChildGroups(id); + const childGroupsResponse = await getChildGroups(id, token); if (childGroupsResponse.error) { return { success: false, error: childGroupsResponse.error }; } @@ -551,7 +568,7 @@ export async function deleteRuleGroup(id: string): Promise<{success: boolean; er // 遍历删除每个子分组 for (const childGroup of childGroups) { - const deleteChildResult = await deleteChildGroup(childGroup.id); + const deleteChildResult = await deleteChildGroup(childGroup.id, token); if (!deleteChildResult.success) { return deleteChildResult; } @@ -559,7 +576,7 @@ export async function deleteRuleGroup(id: string): Promise<{success: boolean; er } // 3. 删除分组下的所有评查点 - const deletePointsResult = await deleteEvaluationPointsByGroupId(id); + const deletePointsResult = await deleteEvaluationPointsByGroupId(id, token); if (!deletePointsResult.success) { return deletePointsResult; } @@ -568,7 +585,8 @@ export async function deleteRuleGroup(id: string): Promise<{success: boolean; er const response = await postgrestDelete>('evaluation_point_groups', { filter: { 'id': `eq.${id}` - } + }, + token }); if (response.error) { @@ -588,12 +606,13 @@ export async function deleteRuleGroup(id: string): Promise<{success: boolean; er /** * 删除子分组及其相关数据 * @param id 子分组ID + * @param token JWT token (可选) * @returns 删除结果 */ -async function deleteChildGroup(id: string): Promise<{success: boolean; error?: string}> { +async function deleteChildGroup(id: string, token?: string): Promise<{success: boolean; error?: string}> { try { // 1. 删除子分组下的所有评查点 - const deletePointsResult = await deleteEvaluationPointsByGroupId(id); + const deletePointsResult = await deleteEvaluationPointsByGroupId(id, token); if (!deletePointsResult.success) { return deletePointsResult; } @@ -602,7 +621,8 @@ async function deleteChildGroup(id: string): Promise<{success: boolean; error?: const response = await postgrestDelete>('evaluation_point_groups', { filter: { 'id': `eq.${id}` - } + }, + token }); if (response.error) { @@ -622,14 +642,16 @@ async function deleteChildGroup(id: string): Promise<{success: boolean; error?: /** * 删除指定分组下的所有评查点 * @param groupId 分组ID + * @param token JWT token (可选) * @returns 删除结果 */ -async function deleteEvaluationPointsByGroupId(groupId: string): Promise<{success: boolean; error?: string}> { +async function deleteEvaluationPointsByGroupId(groupId: string, token?: string): Promise<{success: boolean; error?: string}> { try { const response = await postgrestDelete>('evaluation_points', { filter: { 'evaluation_point_groups_id': `eq.${groupId}` - } + }, + token }); if (response.error) { diff --git a/app/api/evaluation_points/rules-files.ts b/app/api/evaluation_points/rules-files.ts index ec7f06c..f809967 100644 --- a/app/api/evaluation_points/rules-files.ts +++ b/app/api/evaluation_points/rules-files.ts @@ -100,6 +100,7 @@ export interface DocumentSearchParams { sortOrder?: string; // 排序方式 page?: number; // 当前页码 pageSize?: number; // 每页条数 + token?: string; // JWT token } @@ -168,7 +169,8 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do reviewStatus, dateFrom, dateTo, - sortOrder = 'upload_time_desc' + sortOrder = 'upload_time_desc', + token } = searchParams; let p_typeid: number[] | null = null; @@ -204,8 +206,8 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do // 并行执行获取数据和获取总数的请求 const [filesResponse, countResponse] = await Promise.all([ - postgrestPost('rpc/get_review_files_with_details', listParams), - postgrestPost('rpc/count_review_files', rpcParams) + postgrestPost('rpc/get_review_files_with_details', listParams, token), + postgrestPost('rpc/count_review_files', rpcParams, token) ]); // 处理获取文档列表的错误 @@ -316,9 +318,10 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}, do * @param id 文件ID * @param auditStatus 审核状态 * @param userId 用户ID + * @param token JWT token (可选) * @returns 更新结果 */ -export async function updateDocumentAuditStatus(id: string, auditStatus: number, userId: string): Promise<{ +export async function updateDocumentAuditStatus(id: string, auditStatus: number, userId: string, token?: string): Promise<{ success?: boolean; error?: string; status?: number; @@ -338,7 +341,8 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number, { id: parseInt(id), user_id: parseInt(userId) // 确保只能更新自己的文档 - } + }, + token ); if (response.error) { diff --git a/app/api/evaluation_points/rules.ts b/app/api/evaluation_points/rules.ts index 23efce8..73b0696 100644 --- a/app/api/evaluation_points/rules.ts +++ b/app/api/evaluation_points/rules.ts @@ -35,6 +35,7 @@ export interface RulesQueryParams { orderBy?: string; orderDirection?: 'asc' | 'desc'; reviewType?: string; // 添加 reviewType 参数,值为 contract 或 record + token?: string; // JWT token } /** @@ -164,7 +165,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul keyword, orderBy = 'created_at', orderDirection = 'desc', - reviewType + reviewType, + token } = params; // 构建PostgrestParams参数 @@ -194,7 +196,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul // 添加额外头部,用于获取总记录数 headers: { 'Prefer': 'count=exact' - } + }, + token }; // 添加精确匹配过滤:规则组ID @@ -211,7 +214,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul try { // 先获取所有评查点组数据,用于找到对应的pid const groupsAllResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number; pid: number}>}>('evaluation_point_groups', { - select: 'id,pid' + select: 'id,pid', + token }); let groups: Array<{id: number; pid: number}> = []; @@ -254,7 +258,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul select: 'id', filter: { 'pid': `eq.${ruleType}` - } + }, + token }; const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_point_groups', groupsParams); @@ -364,7 +369,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul // 使用Promise.all并行查询所有分组信息 - 使用正确的函数名 const groupPromises = validGroupIds.map(id => postgrestGet<{code: number; msg: string; data: {id: number; pid: number; name: string; first_name: string; second_name: string}[]}>( - `rpc/get_evaluation_point_group_with_pid?input_id=${id}` + `rpc/get_evaluation_point_group_with_pid?input_id=${id}`, + { token } ) ); @@ -447,9 +453,10 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul /** * 获取单个评查点详情 * @param id 评查点ID + * @param token JWT token (可选) * @returns 评查点详情 */ -export async function getRule(id: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { +export async function getRule(id: string, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { // 使用postgrestGet获取单个评查点数据 const postgrestParams: PostgrestParams = { @@ -473,7 +480,8 @@ export async function getRule(id: string): Promise<{data: Rule; error?: never} | action_config, created_at, updated_at - ` + `, + token }; // 获取评查点详情 @@ -498,7 +506,8 @@ export async function getRule(id: string): Promise<{data: Rule; error?: never} | select: 'id,name', filter: { 'id': `eq.${apiRule.evaluation_point_groups_id}` - } + }, + token }; // 查询评查点分组 @@ -538,9 +547,10 @@ export async function getRule(id: string): Promise<{data: Rule; error?: never} | /** * 创建新评查点 * @param ruleData 评查点数据 + * @param token JWT token (可选) * @returns 创建的评查点 */ -export async function createRule(ruleData: Omit): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { +export async function createRule(ruleData: Omit, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { // 将前端模型转换为API接受的格式 const apiRuleData = { @@ -569,7 +579,7 @@ export async function createRule(ruleData: Omit('evaluation_points', apiRuleData); + const response = await postgrestPost<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>('evaluation_points', apiRuleData, token); // 检查是否有错误响应 if (response.error) { @@ -598,9 +608,10 @@ export async function createRule(ruleData: Omit>): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { +export async function updateRule(id: string, ruleData: Partial>, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { // 构建API接受的更新数据 const apiRuleData: Record = {}; @@ -630,7 +641,7 @@ export async function updateRule(id: string, ruleData: Partial(`evaluation_points/${id}`, apiRuleData); + const response = await postgrestPut<{code: number; msg: string; data: ApiRule}, typeof apiRuleData>(`evaluation_points/${id}`, apiRuleData, undefined, token); // 检查是否有错误响应 if (response.error) { @@ -658,9 +669,10 @@ export async function updateRule(id: string, ruleData: Partial { +export async function deleteRule(id: string, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { // console.log(`开始删除评查点, ID: ${id}`); @@ -671,7 +683,8 @@ export async function deleteRule(id: string): Promise<{data: Rule; error?: never }, headers: { 'Prefer': 'return=representation' // 请求返回被删除的记录 - } + }, + token }; // 使用postgrestDelete删除评查点 @@ -771,12 +784,13 @@ export async function deleteRule(id: string): Promise<{data: Rule; error?: never /** * 复制评查点 * @param id 评查点ID + * @param token JWT token (可选) * @returns 新创建的评查点 */ -export async function duplicateRule(id: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { +export async function duplicateRule(id: string, token?: string): Promise<{data: Rule; error?: never} | {data?: never; error: string; status?: number}> { try { // 1. 获取原评查点详情 - const ruleResponse = await getRule(id); + const ruleResponse = await getRule(id, token); if (ruleResponse.error || !ruleResponse.data) { return { error: ruleResponse.error || '获取评查点详情失败', status: 500 }; @@ -798,7 +812,7 @@ export async function duplicateRule(id: string): Promise<{data: Rule; error?: ne }; // 3. 创建新评查点 - return createRule(newRuleData); + return createRule(newRuleData, token); } catch (error) { console.error('复制评查点出错:', error); @@ -833,9 +847,10 @@ export interface RuleGroup { /** * 获取评查点类型列表 * @param reviewType 评查类型,contract表示合同,record表示卷宗 + * @param token JWT token (可选) * @returns 评查点类型列表 */ -export async function getRuleTypes(reviewType?: string): Promise<{data: RuleType[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getRuleTypes(reviewType?: string, token?: string): Promise<{data: RuleType[]; error?: never} | {data?: never; error: string; status?: number}> { try { // 构建PostgrestParams参数 const postgrestParams: PostgrestParams = { @@ -850,7 +865,8 @@ export async function getRuleTypes(reviewType?: string): Promise<{data: RuleType // 查询父ID为0的类型(顶级类型) filter: { 'pid': 'eq.0' - } + }, + token }; // 根据 reviewType 添加过滤条件 @@ -919,9 +935,10 @@ export async function getRuleTypes(reviewType?: string): Promise<{data: RuleType /** * 根据评查点类型ID获取规则组列表 * @param typeId 评查点类型ID + * @param token JWT token (可选) * @returns 规则组列表 */ -export async function getRuleGroupsByType(typeId: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getRuleGroupsByType(typeId: string, token?: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { try { // 如果typeId为空或为"全部",则返回空数组 if (!typeId || typeId === 'all') { @@ -941,7 +958,8 @@ export async function getRuleGroupsByType(typeId: string): Promise<{data: RuleGr // 查询指定类型ID的规则组 filter: { 'pid': `eq.${typeId}` - } + }, + token }; // 发送请求获取规则组列表 diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 2a3aac7..6dfe3ea 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -39,6 +39,7 @@ export interface DocumentSearchParams { pageSize?: number; reviewType?: string; userId?: string; // 添加用户ID筛选 + token?: string; // JWT token } /** @@ -88,6 +89,7 @@ export interface DocumentUI { fileType: string; path: string; isTest: boolean; + remark?: string; updatedAt?: string; pageCount?: number; ocrResult?: unknown; @@ -108,11 +110,12 @@ function getFileExtension(filename: string): string { * @param id 评查结果ID * @returns 评查结果 */ -async function getEvaluationResults(id: number) { +async function getEvaluationResults(id: number, frontendJWT?: string) { const response = await postgrestGet<[]>('evaluation_results', { filter: { 'document_id': `eq.${id}` - } + }, + token: frontendJWT }); if (response.error) { return { error: response.error, status: response.status }; @@ -125,12 +128,12 @@ async function getEvaluationResults(id: number) { /** * 将API文档转换为UI文档 */ -async function convertToUIDocument(doc: Document): Promise { +async function convertToUIDocument(doc: Document, frontendJWT?: string): Promise { // 获取文档类型信息 - const typeResponse = await getDocumentTypes(); + const typeResponse = await getDocumentTypes(undefined, frontendJWT); const documentTypes = typeResponse.data?.types || []; const docType = documentTypes.find(type => type.id.toString() === doc.type_id.toString()); - const evaluationResult = await getEvaluationResults(doc.id); + const evaluationResult = await getEvaluationResults(doc.id, frontendJWT); let issues = 0; interface EvaluationResultItem { @@ -164,6 +167,7 @@ async function convertToUIDocument(doc: Document): Promise { fileType: getFileExtension(doc.name), path: doc.path, isTest: doc.is_test_document, + remark: doc.remark, updatedAt: formatDate(doc.updated_at), pageCount: doc.ocr_result?.__meta?.page_count || 0, ocrResult: doc.ocr_result @@ -216,7 +220,8 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro dateFrom, dateTo, reviewType, - userId + userId, + token } = searchParams; let documentTypes: number[] | undefined; @@ -248,8 +253,8 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro // 并行执行获取数据和获取总数的请求 const [documentsResponse, countResponse] = await Promise.all([ - postgrestPost('rpc/get_documents_with_filters', { ...rpcParams, page, page_size: pageSize }), - postgrestPost('rpc/count_documents_with_filters', rpcParams) + postgrestPost('rpc/get_documents_with_filters', { ...rpcParams, page, page_size: pageSize }, token), + postgrestPost('rpc/count_documents_with_filters', rpcParams, token) ]); // 处理获取文档列表的错误 @@ -305,9 +310,10 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro * 删除文档 * @param id 文档ID * @param userId 用户ID + * @param token JWT token (可选) * @returns 删除结果 */ -export async function deleteDocument(id: string, userId: string): Promise<{ +export async function deleteDocument(id: string, userId: string, token?: string): Promise<{ success?: boolean; error?: string; status?: number; @@ -327,7 +333,8 @@ export async function deleteDocument(id: string, userId: string): Promise<{ filter: { 'id': `eq.${id}`, 'user_id': `eq.${userId}` // 确保只能删除自己的文档 - } + }, + token } ); @@ -350,7 +357,7 @@ export async function deleteDocument(id: string, userId: string): Promise<{ * @param id 文档ID * @returns 文档详情 */ -export async function getDocument(id: string, userId: string): Promise<{ +export async function getDocument(id: string, userId: string, frontendJWT?: string): Promise<{ data?: DocumentUI; error?: string; status?: number; @@ -371,7 +378,8 @@ export async function getDocument(id: string, userId: string): Promise<{ 'id': `eq.${id}`, 'user_id': `eq.${userId}` }, - limit: 1 + limit: 1, + token: frontendJWT } ); @@ -384,7 +392,7 @@ export async function getDocument(id: string, userId: string): Promise<{ return { error: '文档不存在', status: 404 }; } - const documentUI = await convertToUIDocument(extractedData[0]); + const documentUI = await convertToUIDocument(extractedData[0], frontendJWT); return { data: documentUI }; } catch (error) { @@ -402,7 +410,7 @@ export async function getDocument(id: string, userId: string): Promise<{ * @param id 文档ID * @returns 文档详情 */ -export async function getDocumentWithNoUserId(id: string): Promise<{ +export async function getDocumentWithNoUserId(id: string, frontendJWT?: string): Promise<{ data?: DocumentUI; error?: string; status?: number; @@ -418,7 +426,8 @@ export async function getDocumentWithNoUserId(id: string): Promise<{ filter: { 'id': `eq.${id}`, }, - limit: 1 + limit: 1, + token: frontendJWT } ); @@ -432,7 +441,7 @@ export async function getDocumentWithNoUserId(id: string): Promise<{ } // console.log('extractedData', extractedData); - const documentUI = await convertToUIDocument(extractedData[0]); + const documentUI = await convertToUIDocument(extractedData[0], frontendJWT); return { data: documentUI }; } catch (error) { @@ -488,7 +497,7 @@ export async function getFileDownloadUrl(filePath: string): Promise<{ * @param document 部分文档数据 * @returns 更新结果 */ -export async function updateDocument(id: string, document: Partial & { remark?: string }, userId: string): Promise<{ +export async function updateDocument(id: string, document: Partial & { remark?: string }, userId: string, frontendJWT?: string): Promise<{ data?: DocumentUI; error?: string; status?: number; @@ -533,7 +542,8 @@ export async function updateDocument(id: string, document: Partial & { id: parseInt(id), user_id: parseInt(userId) // 确保只能更新自己的文档 - } + }, + frontendJWT ); if (response.error) { @@ -542,7 +552,7 @@ export async function updateDocument(id: string, document: Partial & } // 获取更新后的完整文档数据 - const updatedResponse = await getDocument(id, userId); + const updatedResponse = await getDocument(id, userId, frontendJWT); return updatedResponse; } catch (error) { diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index 75d5ed8..57445cd 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -357,12 +357,19 @@ export async function uploadDocumentToServer( // const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, { try { // console.log('【调试】开始fetch请求...'); + + // 构建请求头,只在有JWT token时添加Authorization + const headers: HeadersInit = { + 'X-File-Name': encodeURIComponent(fileName) + }; + + if (jwtToken) { + headers['Authorization'] = `Bearer ${jwtToken}`; + } + const response = await fetch(uploadUrl, { method: 'POST', - headers: { - 'X-File-Name': encodeURIComponent(fileName), - 'Authorization': `Bearer ${jwtToken || ''}` - }, + headers, body: formData }); @@ -422,7 +429,7 @@ export async function uploadDocumentToServer( * @param reviewType 审核类型(可选) * @returns 文档列表 */ -export async function getTodayDocuments(userInfo?: { user_id?: number; [key: string]: unknown }, reviewType?: string): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getTodayDocuments(userInfo?: { user_id?: number; [key: string]: unknown }, reviewType?: string, token?: string): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { try { // 检查用户信息是否存在 if (!userInfo?.user_id) { @@ -492,7 +499,7 @@ export async function getTodayDocuments(userInfo?: { user_id?: number; [key: str // postgrestGet('contract_structure_comparison', comparisonParams) // ]); - const documentsResponse = await postgrestGet('documents', documentsParams); + const documentsResponse = await postgrestGet('documents', { ...documentsParams, token }); // console.log('documents表响应:', documentsResponse); // console.log('contract_structure_comparison表响应:', comparisonResponse); @@ -594,7 +601,7 @@ export async function getTodayDocuments(userInfo?: { user_id?: number; [key: str } // console.log('发送请求参数:', params); - const response = await postgrestGet('documents', params); + const response = await postgrestGet('documents', { ...params, token }); // console.log('API 响应:', response); if (response.error) { @@ -623,9 +630,10 @@ export async function getTodayDocuments(userInfo?: { user_id?: number; [key: str /** * 获取文档类型列表 * @param reviewType 审核类型(可选) + * @param token JWT token (可选) * @returns 文档类型列表 */ -export async function getDocumentTypes(reviewType?: string): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getDocumentTypes(reviewType?: string, token?: string): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> { try { const params: PostgrestParams = { select: 'id, name', @@ -649,7 +657,7 @@ export async function getDocumentTypes(reviewType?: string): Promise<{data: Docu } } - const response = await postgrestGet('document_types', params); + const response = await postgrestGet('document_types', { ...params, token }); if (response.error) { return { error: response.error, status: response.status }; @@ -674,11 +682,13 @@ export async function getDocumentTypes(reviewType?: string): Promise<{data: Docu * 获取指定文档的状态 * @param documentIds 文档ID列表 * @param attachmentIds 合同附件ID列表(可选) + * @param token JWT token (可选) * @returns 文档状态列表 */ export async function getDocumentsStatus( documentIds: number[], - attachmentIds?: number[] + attachmentIds?: number[], + token?: string ): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { try { if ((!documentIds || documentIds.length === 0) && (!attachmentIds || attachmentIds.length === 0)) { @@ -695,7 +705,7 @@ export async function getDocumentsStatus( 'id': `in.(${documentIds.join(',')})` } }; - documentsResponse = await postgrestGet('documents', documentsParams); + documentsResponse = await postgrestGet('documents', { ...documentsParams, token }); } // 查询合同附件状态 @@ -708,7 +718,7 @@ export async function getDocumentsStatus( 'id': `in.(${attachmentIds.join(',')})` } }; - attachmentResponse = await postgrestGet('contract_structure_comparison', attachmentParams); + attachmentResponse = await postgrestGet('contract_structure_comparison', { ...attachmentParams, token }); } if (documentsResponse.error && attachmentResponse.error) { diff --git a/app/api/home/home.ts b/app/api/home/home.ts index 615a5b0..e04daea 100644 --- a/app/api/home/home.ts +++ b/app/api/home/home.ts @@ -94,9 +94,11 @@ function buildTypeFilter(reviewType: string | null): string { /** * 获取主页数据 * @param reviewType 从客户端传入的 reviewType 值 + * @param userId 用户ID + * @param token JWT token * @returns 主页数据 */ -export async function getHomeData(reviewType?: string | null,userId?: string | number): Promise { +export async function getHomeData(reviewType?: string | null,userId?: string | number, token?: string): Promise { try { // 获取当前日期和时间相关值 const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'); @@ -170,7 +172,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n } const todayPendingCount = await handleApiResponse<{ count: number }[]>( - postgrestGet('documents', todayPendingParams), + postgrestGet('documents', { ...todayPendingParams, token }), '获取今日待审核文件数量失败', [] ); @@ -201,7 +203,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n } const thisMonthReviewedCount = await handleApiResponse<{ count: number }[]>( - postgrestGet('documents', thisMonthReviewedParams), + postgrestGet('documents', { ...thisMonthReviewedParams, token }), '获取本月已审核文件数量失败', [] ); @@ -237,7 +239,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n } const lastMonthReviewedCount = await handleApiResponse<{ count: number }[]>( - postgrestGet('documents', lastMonthReviewedParams), + postgrestGet('documents', { ...lastMonthReviewedParams, token }), '获取上月已审核文件数量失败', [] ); @@ -283,7 +285,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n } const thisMonthTotalCount = await handleApiResponse<{ count: number }[]>( - postgrestGet('documents', thisMonthTotalParams), + postgrestGet('documents', { ...thisMonthTotalParams, token }), '获取本月审核通过数量失败', [] ); @@ -323,7 +325,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n } const lastMonthTotalCount = await handleApiResponse<{ count: number }[]>( - postgrestGet('documents', lastMonthTotalParams), + postgrestGet('documents', { ...lastMonthTotalParams, token }), '获取上月审核通过数量失败', [] ); @@ -373,7 +375,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n end_time: endOfThisMonth, type_val: typeToQuery, userid: parseInt(userId as string) - }), + }, token), '获取合同本月问题数据失败', [] ); @@ -388,7 +390,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n end_time: endOfLastMonth, type_val: typeToQuery, userid: parseInt(userId as string) - }), + }, token), '获取上月问题数据失败', [] ); @@ -406,7 +408,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n end_time: endOfThisMonth, type_val: typeToQuery, userid: parseInt(userId as string) - }), + }, token), '获取本月许可卷宗类型2问题数据失败', [] ); @@ -422,7 +424,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n end_time: endOfLastMonth, type_val: typeToQuery, userid: parseInt(userId as string) - }), + }, token), '获取上月许可卷宗类型2问题数据失败', [] ); diff --git a/app/api/jwt-helper.server.ts b/app/api/jwt-helper.server.ts new file mode 100644 index 0000000..7cee762 --- /dev/null +++ b/app/api/jwt-helper.server.ts @@ -0,0 +1,30 @@ +// app/api/jwt-helper.server.ts +import { getUserSession } from './login/auth.server'; + +/** + * 从 request 中获取 JWT token + * @param request Remix Request 对象 + * @returns JWT token 或 undefined + */ +export async function getJwtFromRequest(request: Request): Promise { + const { frontendJWT } = await getUserSession(request); + return frontendJWT || undefined; +} + +/** + * 包装 PostgrestParams,自动添加 JWT + * @param request Remix Request 对象 + * @param params 原始参数 + * @returns 包含 JWT 的参数 + */ +export async function withJwt( + request: Request, + params?: T +): Promise { + const jwt = await getJwtFromRequest(request); + return { + ...params as T, + token: jwt + }; +} + diff --git a/app/api/login/auth.server.ts b/app/api/login/auth.server.ts index 6ba7a6f..b74b8eb 100644 --- a/app/api/login/auth.server.ts +++ b/app/api/login/auth.server.ts @@ -526,10 +526,10 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise */ -export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolean, data?: SsoUser, error?: string}> { +export async function saveUserInfo(userInfo: UserInfo, token?: string): Promise<{success: boolean, data?: SsoUser, error?: string}> { try { console.log("开始保存用户信息", userInfo); - + // 验证必要字段 if (!userInfo.sub) { return { success: false, error: "用户唯一标识 sub 不能为空" }; @@ -540,7 +540,8 @@ export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolea filter: { "sub": `eq.${userInfo.sub}`, "deleted_at": "is.null" // 只查询未删除的记录 - } + }, + token }); if (existingUserResult.error) { @@ -572,7 +573,8 @@ export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolea const updateResult = await postgrestPut>( "sso_users", userData, - { id: existingUser.id! } + { id: existingUser.id! }, + token ); if (updateResult.error) { @@ -589,7 +591,7 @@ export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolea // 3. 用户不存在,执行插入操作 同时需要给这个用户默认添加一个角色,角色为common console.log("用户不存在,执行插入操作"); - const insertResult = await postgrestPost("sso_users", userData as SsoUser); + const insertResult = await postgrestPost("sso_users", userData as SsoUser, token); if (insertResult.error) { console.error("插入用户失败:", insertResult.error); @@ -601,7 +603,7 @@ export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolea // 4. 给这个用户默认添加一个角色,角色为common const userData_with_id = Array.isArray(insertResult.data) ? insertResult.data[0] : insertResult.data as unknown as SsoUser; if (userData_with_id?.id) { - await addDefaultRole(userData_with_id.id, 2); + await addDefaultRole(userData_with_id.id, 2, token); } return { @@ -620,21 +622,23 @@ export async function saveUserInfo(userInfo: UserInfo): Promise<{success: boolea /** * 为用户添加默认角色 - * + * * @param userId - 用户ID * @param roleId - 角色ID,默认为2(common角色) + * @param token - JWT令牌,用于调用postgrest服务 * @returns 添加结果 */ -export async function addDefaultRole(userId: string, roleId: number = 2) { +export async function addDefaultRole(userId: string, roleId: number = 2, token?: string) { try { console.log(`为用户 ${userId} 添加默认角色 ${roleId}`); - + // 检查用户是否已经有此角色 const existingRoleResult = await postgrestGet>("user_role", { filter: { user_id: `eq.${userId}`, role_id: `eq.${roleId}` - } + }, + token }); if (existingRoleResult.error) { @@ -652,7 +656,7 @@ export async function addDefaultRole(userId: string, roleId: number = 2) { const addRoleResult = await postgrestPost, {user_id: string, role_id: number}>("user_role", { user_id: userId, role_id: roleId - }); + }, token); if (addRoleResult.error) { console.error("添加用户角色失败:", addRoleResult.error); @@ -749,11 +753,16 @@ export async function simpleRootLogin( }); const loginResult = await loginResponse.json(); + console.log('登录接口返回', loginResult); + + // 检查重试次数 + const retryCount = loginResult.retryCount || loginResult.retry_count || 0; + console.log('登录重试次数:', retryCount); if (loginResult.code === 0 && loginResult.data) { // 登录成功,构建用户信息 const userData = loginResult.data; - console.log('管理员登录userData', userData); + // console.log('管理员登录userData', userData); const userRole = userData.role; // 默认角色 // 生成模拟的OAuth token信息 @@ -797,13 +806,28 @@ export async function simpleRootLogin( frontendJWT }); } else { - // 登录失败,返回错误信息 - const errorMsg = loginResult.msg || "登录失败,请检查用户名和密码"; + // 登录失败,检查账户是否被锁定 + let errorMsg = loginResult.msg || "登录失败,请检查用户名和密码"; + let isLocked = false; + + // 检查是否因重试次数过多被锁定 + if (retryCount >= 5) { + errorMsg = "账户已被锁定,密码错误次数过多,请联系管理员"; + isLocked = true; + } else if (retryCount > 0) { + // 显示剩余尝试次数 + const remainingAttempts = 5 - retryCount; + errorMsg = `${loginResult.msg || "用户名或密码错误"},还有 ${remainingAttempts} 次尝试机会`; + } + return new Response(JSON.stringify({ success: false, - error: errorMsg + error: errorMsg, + retryCount: retryCount, + isLocked: isLocked, + remainingAttempts: isLocked ? 0 : (5 - retryCount) }), { - status: 401, + status: isLocked ? 403 : 401, // 403 表示禁止访问(账户被锁) headers: { "Content-Type": "application/json" } }); } diff --git a/app/api/login/token-manager.server.ts b/app/api/login/token-manager.server.ts index 1e051e0..d7fac8f 100644 --- a/app/api/login/token-manager.server.ts +++ b/app/api/login/token-manager.server.ts @@ -6,7 +6,7 @@ * 2. 如果需要新的网络请求,在 `OAuthClient` 中添加 */ import { OAuthClient } from "./oauth-client"; -import { OAUTH_CONFIG } from "~/config/api-config"; +import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server"; interface TokenInfo { accessToken: string; @@ -29,7 +29,9 @@ export class TokenManager { private oauthClient: OAuthClient; constructor() { - this.oauthClient = new OAuthClient(OAUTH_CONFIG); + // 🔒 安全:使用服务器端专用函数获取包含 clientSecret 的完整配置 + // 从 .server.ts 文件中运行时读取,确保环境变量正确加载 + this.oauthClient = new OAuthClient(getServerOAuthConfigRuntime()); } /** diff --git a/app/api/postgrest-client.ts b/app/api/postgrest-client.ts index 9dffb78..5ee6eba 100644 --- a/app/api/postgrest-client.ts +++ b/app/api/postgrest-client.ts @@ -1,7 +1,42 @@ // app/api/postgrest-client.ts +// import { AsyncLocalStorage } from 'async_hooks'; import { apiRequest, type QueryParams } from './axios-client'; import { handleApiError } from './error-handler'; +/** + * 请求上下文接口 + */ +// interface RequestContext { +// jwt?: string; +// } + +/** + * 创建异步本地存储用于存储请求上下文 + */ +// const requestContext = new AsyncLocalStorage(); + +/** + * 在指定的上下文中运行函数 + * @param context 上下文对象 + * @param fn 要执行的函数 + * @returns 函数执行结果 + */ +// export function runWithContext( +// context: RequestContext, +// fn: () => T | Promise +// ): T | Promise { +// return requestContext.run(context, fn); +// } + +/** + * 获取当前上下文中的 JWT + * @returns JWT token 或 undefined + */ +// function getContextJWT(): string | undefined { +// const context = requestContext.getStore(); +// return context?.jwt; +// } + /** * PostgresREST 特定的查询参数接口 */ @@ -23,6 +58,8 @@ export interface PostgrestParams { [key: string]: unknown; // 自定义头部参数 headers?: Record; + // JWT Token(自动添加到 Authorization 头部) + token?: string; } /** @@ -48,6 +85,38 @@ function decodeUrlForDisplay(url: string): string { } } +/** + * 合并 JWT Token 到请求头 + * @param existingHeaders 已有的请求头 + * @param explicitToken 显式传入的 JWT Token(可选) + * @returns 合并后的请求头 + */ +function mergeAuthHeaders( + existingHeaders: Record = {}, + explicitToken?: string +): Record { + const headers = { ...existingHeaders }; + + // 如果已经有 Authorization 头部(不区分大小写),不覆盖 + const hasAuth = Object.keys(headers).some( + key => key.toLowerCase() === 'authorization' + ); + + if (hasAuth) { + return headers; + } + + // 优先使用显式传入的 token,否则从上下文获取 + const token = explicitToken || 'undefined'; + + // 如果有 token(显式传入或从上下文获取),添加到 Authorization 头部 + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + return headers; +} + /** * 打印 PostgREST 查询日志 * @param endpoint 端点 @@ -167,8 +236,8 @@ export function transformParams(params: PostgrestParams): QueryParams { // 处理其他额外参数 Object.entries(params).forEach(([key, value]) => { - // 跳过已处理的特殊参数 - if (!['select', 'order', 'limit', 'offset', 'filter', 'schema', 'or'].includes(key) && value !== undefined) { + // 跳过已处理的特殊参数(包括 headers 和 token) + if (!['select', 'order', 'limit', 'offset', 'filter', 'schema', 'or', 'headers', 'token'].includes(key) && value !== undefined) { result[key] = value as string | number | boolean; } }); @@ -179,7 +248,7 @@ export function transformParams(params: PostgrestParams): QueryParams { /** * 发送 GET 请求到 PostgresREST 接口 * @param endpoint 端点 - * @param params 查询参数 + * @param params 查询参数(可包含 token 和 headers) * @returns 响应数据 */ export async function postgrestGet(endpoint: string, params?: PostgrestParams): Promise<{data: T; headers?: Record; error?: never} | {data?: never; error: string; status?: number}> { @@ -191,13 +260,8 @@ export async function postgrestGet(endpoint: string, params?: PostgrestParams // 打印查询信息 logPostgrestQuery(apiEndpoint, queryParams, 'GET'); - // 提取并移除自定义头部参数 - const headers: Record = params?.headers || {}; - - // 清除查询参数中的headers属性,避免将其作为URL参数 - if (queryParams.headers) { - delete queryParams.headers; - } + // 合并 JWT Token 到请求头 + const headers = mergeAuthHeaders(params?.headers, params?.token); const response = await apiRequest( apiEndpoint, @@ -215,7 +279,7 @@ export async function postgrestGet(endpoint: string, params?: PostgrestParams // 返回数据和响应头 return { data: response.data as T, - headers: response.headers // 假设apiRequest函数已返回响应头 + headers: response.headers }; } catch (error) { const apiError = handleApiError(error); @@ -314,9 +378,10 @@ function handlePostgresError(error: unknown, responseText?: string): { message: * 发送 POST 请求到 PostgresREST 接口 * @param endpoint 端点(表名) * @param data 请求体数据 + * @param token JWT Token(可选) * @returns 响应数据 */ -export async function postgrestPost>(endpoint: string, data: D): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { +export async function postgrestPost>(endpoint: string, data: D, token?: string): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; @@ -332,17 +397,20 @@ export async function postgrestPost>(endpoint: st // console.log(`准备发送 PostgreSQL 插入请求到: ${apiEndpoint}`); // console.log(`请求体: ${requestBody}`); + // 合并 JWT Token 到请求头 + const headers = mergeAuthHeaders({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Prefer': 'return=representation' + }, token); + try { const response = await apiRequest( apiEndpoint, { method: 'POST', body: requestBody, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Prefer': 'return=representation' - } + headers: headers } ); @@ -434,12 +502,14 @@ function preprocessData(data: Record): Record * @param endpoint 端点 * @param data 请求体数据 * @param filters 过滤条件 + * @param token JWT Token(可选) * @returns 响应数据 */ export async function postgrestPut( endpoint: string, data: D, - filters?: Record + filters?: Record, + token?: string ): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 @@ -459,14 +529,17 @@ export async function postgrestPut( // 打印查询信息 logPostgrestQuery(fullEndpoint, queryParams, 'PATCH', data as unknown as Record); + // 合并 JWT Token 到请求头 + const headers = mergeAuthHeaders({ + 'Prefer': 'return=representation' + }, token); + const response = await apiRequest( fullEndpoint, { method: 'PATCH', body: JSON.stringify(data), - headers: { - 'Prefer': 'return=representation' - } + headers: headers }, queryParams ); @@ -489,7 +562,7 @@ export async function postgrestPut( /** * 发送 DELETE 请求到 PostgresREST 接口 * @param endpoint 端点 - * @param params 查询参数,用于指定要删除的记录 + * @param params 查询参数,用于指定要删除的记录(可包含 token 和 headers) * @returns 响应数据 */ export async function postgrestDelete(endpoint: string, params?: PostgrestParams): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { @@ -500,16 +573,11 @@ export async function postgrestDelete(endpoint: string, params?: PostgrestPar // 转换查询参数 const queryParams = params ? transformParams(params) : {}; - // 提取并移除自定义头部参数 - const headers: Record = { + // 合并 JWT Token 到请求头 + const headers = mergeAuthHeaders({ 'Prefer': 'return=representation', // 默认请求返回被删除的记录 ...(params?.headers || {}) - }; - - // 清除查询参数中的headers属性,避免将其作为URL参数 - if (queryParams.headers) { - delete queryParams.headers; - } + }, params?.token); // 打印查询信息 logPostgrestQuery(apiEndpoint, queryParams, 'DELETE'); diff --git a/app/api/prompts/prompts.ts b/app/api/prompts/prompts.ts index e272788..6a41eb9 100644 --- a/app/api/prompts/prompts.ts +++ b/app/api/prompts/prompts.ts @@ -113,9 +113,10 @@ export function convertToUITemplate(template: PromptTemplate): PromptTemplateUI /** * 获取提示词模板列表 * @param searchParams 搜索参数 + * @param frontendJWT JWT token (可选) * @returns 提示词模板列表和总数 */ -export async function getPromptTemplates(searchParams: PromptSearchParams = {}): Promise<{ +export async function getPromptTemplates(searchParams: PromptSearchParams = {}, frontendJWT?: string): Promise<{ data?: { templates: PromptTemplateUI[], total: number }; error?: string; status?: number; @@ -147,7 +148,8 @@ export async function getPromptTemplates(searchParams: PromptSearchParams = {}): }, limit: pageSize, offset: (page - 1) * pageSize, - filter: {} as Record + filter: {} as Record, + token: frontendJWT }; // 添加筛选条件 @@ -226,9 +228,10 @@ export async function getPromptTemplates(searchParams: PromptSearchParams = {}): /** * 获取提示词模板详情 * @param id 模板ID + * @param frontendJWT JWT token (可选) * @returns 提示词模板详情 */ -export async function getPromptTemplate(id: string): Promise<{ +export async function getPromptTemplate(id: string, frontendJWT?: string): Promise<{ data?: PromptTemplateUI; error?: string; status?: number; @@ -254,7 +257,8 @@ export async function getPromptTemplate(id: string): Promise<{ `, filter: { 'id': `eq.${id}` - } + }, + token: frontendJWT }; const response = await postgrestGet('prompt_templates', params); @@ -282,9 +286,10 @@ export async function getPromptTemplate(id: string): Promise<{ /** * 创建提示词模板 * @param template 提示词模板数据 + * @param frontendJWT JWT token (可选) * @returns 创建的提示词模板 */ -export async function createPromptTemplate(template: Partial): Promise<{ +export async function createPromptTemplate(template: Partial, frontendJWT?: string): Promise<{ data?: PromptTemplateUI; error?: string; status?: number; @@ -326,7 +331,8 @@ export async function createPromptTemplate(template: Partial): const response = await postgrestPost>( 'prompt_templates', - apiTemplate + apiTemplate, + frontendJWT ); if (response.error) { @@ -353,9 +359,10 @@ export async function createPromptTemplate(template: Partial): * 更新提示词模板 * @param id 模板ID * @param template 提示词模板数据 + * @param frontendJWT JWT token (可选) * @returns 更新后的提示词模板 */ -export async function updatePromptTemplate(id: string, template: Partial): Promise<{ +export async function updatePromptTemplate(id: string, template: Partial, frontendJWT?: string): Promise<{ data?: PromptTemplateUI; error?: string; status?: number; @@ -416,7 +423,8 @@ export async function updatePromptTemplate(id: string, template: Partial>( 'prompt_templates', apiTemplate, - { id } + { id }, + frontendJWT ); if (response.error) { @@ -442,9 +450,10 @@ export async function updatePromptTemplate(id: string, template: Partial { +}, token?: string): Promise<{data: ConfigItem[]; total: number; error?: never} | {data?: never; error: string}> { try { const { name, @@ -90,7 +90,7 @@ export async function getConfigLists(params: { queryParams.filter = filter; // 获取数据 - const response = await postgrestGet('configurations', queryParams); + const response = await postgrestGet('configurations', { ...queryParams, token }); if (response.error) { return { error: response.error }; @@ -132,11 +132,12 @@ export async function getConfigLists(params: { } // 获取配置类型和环境选项 -export async function getConfigOptions(): Promise<{data: {types: string[]; environments: string[]}; error?: never} | {data?: never; error: string}> { +export async function getConfigOptions(token?: string): Promise<{data: {types: string[]; environments: string[]}; error?: never} | {data?: never; error: string}> { try { // 获取类型选项 const typeResponse = await postgrestGet<{type: string}[]>('configurations', { - select: 'type' + select: 'type', + token }); if (typeResponse.error) { @@ -150,7 +151,8 @@ export async function getConfigOptions(): Promise<{data: {types: string[]; envir // 获取环境选项 const envResponse = await postgrestGet<{environment: string}[]>('configurations', { - select: 'environment' + select: 'environment', + token }); if (envResponse.error) { @@ -179,12 +181,13 @@ export async function getConfigOptions(): Promise<{data: {types: string[]; envir } // 获取配置详情 -export async function getConfigDetail(id: string): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> { +export async function getConfigDetail(id: string, token?: string): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> { try { const response = await postgrestGet('configurations', { filter: { 'id': `eq.${id}` - } + }, + token }); if (response.error) { @@ -218,9 +221,9 @@ export async function createConfig(data: { config: Record; is_active: boolean; remark?: string; -}): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> { +}, token?: string): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> { try { - const response = await postgrestPost('configurations', data); + const response = await postgrestPost('configurations', data, token); if (response.error) { return { error: response.error }; @@ -252,11 +255,11 @@ export async function updateConfig(id: string, data: { config: Record; is_active: boolean; remark?: string; -}): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> { +}, token?: string): Promise<{data: ConfigItem; error?: never} | {data?: never; error: string}> { try { const response = await postgrestPut('configurations', data, { id: id.toString() - }); + }, token); if (response.error) { return { error: response.error }; @@ -281,12 +284,13 @@ export async function updateConfig(id: string, data: { } // 更新配置状态 -export async function updateConfigStatus(id: number, is_active: boolean): Promise<{success: boolean; error?: string}> { +export async function updateConfigStatus(id: number, is_active: boolean, token?: string): Promise<{success: boolean; error?: string}> { try { const response = await postgrestPut( 'configurations', { is_active }, - { id: id.toString() } + { id: id.toString() }, + token ); if (response.error) { diff --git a/app/components/cross-checking/DocumentListModal.tsx b/app/components/cross-checking/DocumentListModal.tsx index adeb1c9..07425b6 100644 --- a/app/components/cross-checking/DocumentListModal.tsx +++ b/app/components/cross-checking/DocumentListModal.tsx @@ -27,12 +27,13 @@ interface DocumentListModalProps { total?: number; onPageChange?: (page: number) => void; onPageSizeChange?: (size: number) => void; + frontendJWT?: string; // 新增JWT参数 } -export function DocumentListModal({ - isOpen, - onClose, - title, +export function DocumentListModal({ + isOpen, + onClose, + title, files, onViewFile, loading = false, @@ -41,7 +42,8 @@ export function DocumentListModal({ pageSize = 10, total = 0, onPageChange, - onPageSizeChange + onPageSizeChange, + frontendJWT }: DocumentListModalProps) { // 查看按钮防抖 const [isnavigating,setIsnavigating] = useState(false) @@ -58,9 +60,8 @@ export function DocumentListModal({ // 检查audit_status是否为0,如果是则更新为2 if (auditStatus === 0 || auditStatus === null) { try { - // TODO: 不需要传递userId,直接使用fileId找到对应文档,然后更新文档状态 - // 更新文档状态 - const updatedFile = await updateDocumentAuditStatus(fileId, 2); + // 更新文档状态,传递JWT + const updatedFile = await updateDocumentAuditStatus(fileId, 2, frontendJWT); console.log('更新后的文档状态:', updatedFile); } catch (error) { console.error('更新文件审核状态时出错:', error); @@ -68,7 +69,7 @@ export function DocumentListModal({ return; } } - + // 如果有自定义的查看处理函数,则调用它 if (onViewFile) { setIsnavigating(true) diff --git a/app/components/cross-checking/FilePreview.tsx b/app/components/cross-checking/FilePreview.tsx index e97261b..c2711ec 100644 --- a/app/components/cross-checking/FilePreview.tsx +++ b/app/components/cross-checking/FilePreview.tsx @@ -410,7 +410,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }} > { console.error("PDF加载错误:", error); diff --git a/app/components/layout/Layout.tsx b/app/components/layout/Layout.tsx index 416463c..698a3d7 100644 --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -18,6 +18,7 @@ const REVIEW_TYPE_TO_APP: Record = { interface LayoutProps { children: React.ReactNode; userRole?: UserRole; + frontendJWT?: string; } // 添加一个接口表示路由handle可能包含的属性 @@ -32,7 +33,7 @@ interface Match { data: unknown; } -export function Layout({ children, userRole = 'developer' }: LayoutProps) { +export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '' }: LayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [selectedApp, setSelectedApp] = useState(''); const matches = useMatches() as Match[]; @@ -108,6 +109,7 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) { onToggle={toggleSidebar} userRole={userRole} selectedApp={selectedApp} + frontendJWT={frontendJWT} />
diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 4b4e01f..85f1fea 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -7,6 +7,7 @@ interface SidebarProps { onToggle: () => void; collapsed: boolean; userRole: UserRole; + frontendJWT?: string; selectedApp?: string; // 添加所选应用模块参数 } @@ -31,7 +32,7 @@ const APP_ICON_MAP: Record = { 'model': '/images/icon_assistant.png' }; -export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: SidebarProps) { +export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selectedApp = '' }: SidebarProps) { const location = useLocation(); const [expandedMenus, setExpandedMenus] = useState>({}); const [currentApp, setCurrentApp] = useState(''); // 初始设置为空字符串而不是selectedApp @@ -47,7 +48,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid try { console.log('userRole', userRole); const roleKey = mapUserRoleToRoleKey(userRole); - const result = await getUserRoutesByRole(roleKey); + const result = await getUserRoutesByRole(roleKey, frontendJWT); if (result.success && result.data) { setMenuItems(result.data); @@ -253,7 +254,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid // }) return ( -
+
{ @@ -300,7 +301,7 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
)} -
+
{isLoading || isLoadingRoutes ? ( // 加载中状态显示,保留菜单布局结构
@@ -382,6 +383,19 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid )) )}
+ + {/* 操作手册下载按钮 */} +
); } \ No newline at end of file diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index cb1f6c2..ddba187 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -414,7 +414,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }} > { console.error("PDF加载错误:", error); diff --git a/app/components/reviews/ReviewTabs.tsx b/app/components/reviews/ReviewTabs.tsx index 8b2caa7..02f3e56 100644 --- a/app/components/reviews/ReviewTabs.tsx +++ b/app/components/reviews/ReviewTabs.tsx @@ -9,7 +9,7 @@ import { Modal } from '~/components/ui/Modal'; import { UploadArea, type UploadAreaRef } from '~/components/ui/UploadArea'; import { Button } from '~/components/ui/Button'; import { toastService } from '~/components/ui/Toast'; -import { DOCUMENT_URL } from "~/api/axios-client"; +// import { DOCUMENT_URL } from "~/api/axios-client"; import { uploadFileToBinary, uploadDocumentToServer } from '~/api/files/files-upload'; interface ReviewTabsProps { @@ -61,7 +61,8 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi // 下载原文件 const handleDownloadFile = async () => { try { - const downloadUrl = `${DOCUMENT_URL}${fileInfo.path}`; + // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 + const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(fileInfo.path || '')}`; // 使用fetch获取文件内容 const response = await fetch(downloadUrl); diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 153c3e8..66419aa 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -3,7 +3,6 @@ * 统一管理所有API地址,方便部署时修改 * 支持环境变量覆盖配置 */ - // 环境配置类型 interface ApiConfig { // 主API基础URL @@ -73,9 +72,12 @@ const portConfigs: Record> = { // 主要 // 梅州 '51703': { - baseUrl: 'http://nas.7bm.co:8873', - documentUrl: 'http://nas.7bm.co:8873/docauditai/', - uploadUrl: 'http://nas.7bm.co:8873/admin/documents' + baseUrl: 'http://172.16.0.55:8073', + documentUrl: 'http://172.16.0.55:8073/docauditai/', + uploadUrl: 'http://172.16.0.55:8073/admin/documents' + // baseUrl: 'http://nas.7bm.co:8873', + // documentUrl: 'http://nas.7bm.co:8873/docauditai/', + // uploadUrl: 'http://nas.7bm.co:8873/admin/documents' }, @@ -121,17 +123,12 @@ const configs: Record = { // 开发环境 development: { baseUrl: 'http://172.16.0.55:8000', - // baseUrl: 'http://172.16.0.81:3000', - // baseUrl: 'http://nas.7bm.co:3000', - // documentUrl: 'http://172.16.0.81:9000/docauditai/', documentUrl: 'http://172.16.0.55:8000/docauditai/', uploadUrl: 'http://172.16.0.55:8000/admin/documents', - // uploadUrl: 'http://172.16.0.58:8008/admin/documents', - // uploadUrl: 'http://172.16.0.58:8008/admin/documents', oauth: { serverUrl: 'http://10.79.112.85', // IDaaS服务器地址 - clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', - clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret + clientId: 'none', + clientSecret: 'none', // 需要替换为实际的Client Secret redirectUri: 'http://10.79.97.17/', // 回调地址 appId: 'idaasoauth2' // 应用ID,用于登出 } @@ -140,17 +137,12 @@ const configs: Record = { // 测试环境 testing: { baseUrl: 'http://nas.7bm.co:8873', - // baseUrl: 'http://172.16.0.58:8873', - // baseUrl: 'http://nas.7bm.co:3000', - // documentUrl: 'http://172.16.0.81:9000/docauditai/', documentUrl: 'http://nas.7bm.co:8873/docauditai/', uploadUrl: 'http://nas.7bm.co:8873/admin/documents', - // uploadUrl: 'http://172.16.0.58:8008/admin/documents', - // uploadUrl: 'http://172.16.0.58:8008/admin/documents', oauth: { serverUrl: 'http://10.79.112.85', // IDaaS服务器地址 clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', - clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret + clientSecret: 'placeholder', // 需要替换为实际的Client Secret redirectUri: 'http://10.79.97.17/', // 回调地址 appId: 'idaasoauth2' // 应用ID,用于登出 } @@ -159,7 +151,6 @@ const configs: Record = { // 生产环境 production: { // postgrest - // baseUrl: 'http://172.16.0.55:8008', baseUrl: 'http://10.79.97.17:8000', // minio documentUrl: 'http://10.76.244.156:9000/docauditai/', @@ -168,7 +159,10 @@ const configs: Record = { oauth: { serverUrl: 'http://10.79.112.85', // IDaaS服务器地址 clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', - clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret + // clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret + // ⚠️ 安全警告:clientSecret 不应该硬编码在代码中 + // 请在生产环境使用环境变量 OAUTH_CLIENT_SECRET + clientSecret: 'placeholder', // 占位符,实际值从环境变量获取 redirectUri: 'http://10.79.97.17/', // 回调地址 appId: 'idaasoauth2' // 应用ID,用于登出 } @@ -181,7 +175,7 @@ const configs: Record = { uploadUrl: 'http://172.16.0.119:8000/admin/documents/upload', oauth: { serverUrl: 'http://10.79.112.85', // IDaaS服务器地址 - clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', // 需要替换为实际的Client ID + clientId: 'none', // 需要替换为实际的Client ID clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret redirectUri: 'http://172.16.0.119:3000/callback', // 回调地址 appId: 'idaasoauth2' // 应用ID,用于登出 @@ -204,13 +198,11 @@ const getCurrentEnvironment = (): string => { // 客户端:优先使用NEXT_PUBLIC_前缀的环境变量 const nextPublicNodeEnv = process.env.NEXT_PUBLIC_NODE_ENV; - const nextPublicEnv = process.env.NEXT_PUBLIC_API_ENV; const nodeEnv = process.env.NODE_ENV; - const result = nextPublicNodeEnv || nextPublicEnv || nodeEnv || 'development'; + const result = nextPublicNodeEnv || nodeEnv || 'development'; console.log('🔧 客户端环境检测:', { NEXT_PUBLIC_NODE_ENV: nextPublicNodeEnv, - NEXT_PUBLIC_API_ENV: nextPublicEnv, NODE_ENV: nodeEnv, result: result }); @@ -227,7 +219,9 @@ const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => { oauth: { serverUrl: process.env.NEXT_PUBLIC_OAUTH_SERVER_URL || defaultConfig.oauth.serverUrl, clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID || defaultConfig.oauth.clientId, - clientSecret: process.env.NEXT_PUBLIC_OAUTH_CLIENT_SECRET || defaultConfig.oauth.clientSecret, + // ⚠️ 注意:clientSecret 不应该使用 NEXT_PUBLIC_ 前缀 + // 应该只在服务器端通过 process.env.OAUTH_CLIENT_SECRET 访问 + clientSecret: process.env.OAUTH_CLIENT_SECRET || defaultConfig.oauth.clientSecret, redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI || defaultConfig.oauth.redirectUri, appId: process.env.NEXT_PUBLIC_OAUTH_APP_ID || defaultConfig.oauth.appId } @@ -355,6 +349,18 @@ export const { oauth: OAUTH_CONFIG } = apiConfig; +/** + * 🔓 客户端安全的 OAuth 配置(不包含 clientSecret) + * 可以安全地在客户端代码中使用 + */ +export const CLIENT_OAUTH_CONFIG = { + serverUrl: OAUTH_CONFIG.serverUrl, + clientId: OAUTH_CONFIG.clientId, + redirectUri: OAUTH_CONFIG.redirectUri, + appId: OAUTH_CONFIG.appId, + // 客户端不需要 clientSecret +}; + // 导出所有配置,供调试使用 export { configs }; @@ -378,12 +384,17 @@ export const getCurrentPortConfig = () => { }; // 调试信息(仅在开发环境显示) -if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { +if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'testing') { console.log('📦 API配置信息:', { environment: getCurrentEnvironment(), currentEnv: process.env.NODE_ENV, - nextPublicEnv: process.env.NEXT_PUBLIC_API_ENV, port: getCurrentPort(), - config: apiConfig + config: { + ...apiConfig, + oauth: { + ...apiConfig.oauth, + clientSecret: '***' // 隐藏敏感信息 + } + }, }); } \ No newline at end of file diff --git a/app/config/oauth-secret.server.ts b/app/config/oauth-secret.server.ts new file mode 100644 index 0000000..ea7a78f --- /dev/null +++ b/app/config/oauth-secret.server.ts @@ -0,0 +1,60 @@ +/** + * 🔒 服务器端专用:OAuth 敏感配置 + * + * 此文件只在服务器端运行,确保环境变量在运行时读取 + * Remix 会自动排除 .server.ts 文件不打包到客户端 + */ + +// 用于控制日志输出(避免重复日志) +let hasLoggedSecret = false; + +/** + * 从环境变量获取 OAuth Client Secret + * 在服务器运行时动态读取,不依赖构建时的环境变量注入 + */ +export function getOAuthClientSecret(): string { + const secret = process.env.OAUTH_CLIENT_SECRET; + + // 只在第一次调用时输出详细日志(避免启动时就输出) + if (!hasLoggedSecret) { + hasLoggedSecret = true; + + console.log('🔍 [oauth-secret.server] 读取 OAUTH_CLIENT_SECRET:'); + console.log(' - 值存在:', !!secret); + console.log(' - 值长度:', secret?.length || 0); + console.log(' - 值预览:', secret ? `${secret.substring(0, 10)}...` : 'undefined'); + console.log(' - 是否为占位符:', secret === 'placeholder' || secret === 'none'); + + if (!secret || secret === 'placeholder' || secret === 'none') { + console.warn('⚠️ 警告:未设置有效的 OAUTH_CLIENT_SECRET 环境变量'); + console.warn('⚠️ 当前值:', secret); + console.warn('⚠️ 包含 OAUTH 的环境变量:', Object.keys(process.env).filter(k => k.includes('OAUTH'))); + console.warn('⚠️ 包含 SECRET 的环境变量:', Object.keys(process.env).filter(k => k.includes('SECRET'))); + } else { + console.log('✅ [oauth-secret.server] OAUTH_CLIENT_SECRET 已成功读取'); + } + } + + return secret || ''; +} + +/** + * 获取服务器端 OAuth 配置 + */ +export function getServerOAuthConfigRuntime() { + const secret = getOAuthClientSecret(); + + // 从基础配置中获取其他 OAuth 参数 + const baseConfig = { + serverUrl: process.env.NEXT_PUBLIC_OAUTH_SERVER_URL || 'http://10.79.112.85', + clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID || '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', + redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI || 'http://10.79.97.17/', + appId: process.env.NEXT_PUBLIC_OAUTH_APP_ID || 'idaasoauth2', + }; + + return { + ...baseConfig, + clientSecret: secret + }; +} + diff --git a/app/root.tsx b/app/root.tsx index 69f81fd..c56ad81 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -75,7 +75,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const isPublicPath = publicPaths.some(path => pathname.startsWith(path)); // 获取用户会话(可能包含刷新后的token) - const { isAuthenticated, userRole, refreshedSession } = await getUserSession(request); + const { isAuthenticated, userRole, refreshedSession, frontendJWT } = await getUserSession(request); // console.log("是否公开路径:", isPublicPath, "是否已认证:", isAuthenticated); // 如果访问需要认证的路径但未登录,重定向到登录页 @@ -145,6 +145,7 @@ export async function loader({ request }: LoaderFunctionArgs) { isAuthenticated, userRole, pathname, + frontendJWT, ENV: { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_APP_ID: process.env.NEXT_PUBLIC_APP_ID, @@ -182,7 +183,7 @@ export function links() { } export default function App() { - const { userRole, ENV } = useLoaderData(); + const { userRole, ENV, frontendJWT } = useLoaderData(); return ( @@ -215,7 +216,7 @@ export default function App() { - + diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index ebd317c..021003c 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -51,8 +51,8 @@ export default function Index() { useEffect(() => { if (typeof window !== 'undefined') { - // setIsPort51708(window.location.port === '51708'); - setIsPort51708(window.location.port === '5178'); + setIsPort51708(window.location.port === '51708'); + // setIsPort51708(window.location.port === '5178'); } }, []); diff --git a/app/routes/api.oauth.token.tsx b/app/routes/api.oauth.token.tsx index 50e540d..a0e6328 100644 --- a/app/routes/api.oauth.token.tsx +++ b/app/routes/api.oauth.token.tsx @@ -1,6 +1,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; -import { OAUTH_CONFIG } from "~/config/api-config"; +import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server"; /** * 这个Action作为获取OAuth Access Token的服务器端代理。 @@ -24,7 +24,8 @@ export async function action({ request }: ActionFunctionArgs) { console.log("🔧 [/api/oauth/token] 收到代理请求, code:", code ? `${code.substring(0, 10)}...` : null); // 3. 在服务器端使用OAuthClient安全地获取访问令牌 - const oauthClient = new OAuthClient(OAUTH_CONFIG); + // 🔒 安全:从 .server.ts 文件运行时读取配置,确保环境变量正确加载 + const oauthClient = new OAuthClient(getServerOAuthConfigRuntime()); const tokenResponse = await oauthClient.getAccessToken(code); // 4. 处理来自IDaaS服务器的响应 diff --git a/app/routes/api.oauth.userinfo.tsx b/app/routes/api.oauth.userinfo.tsx index a2c81ec..fcb761a 100644 --- a/app/routes/api.oauth.userinfo.tsx +++ b/app/routes/api.oauth.userinfo.tsx @@ -1,6 +1,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; -import { OAUTH_CONFIG } from "~/config/api-config"; +import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server"; /** * 这个Action作为获取用户信息的服务器端代理。 @@ -20,7 +20,8 @@ export async function action({ request }: ActionFunctionArgs) { console.log("🔧 [/api/oauth/userinfo] 收到代理请求。"); - const oauthClient = new OAuthClient(OAUTH_CONFIG); + // 🔒 安全:从 .server.ts 文件运行时读取配置 + const oauthClient = new OAuthClient(getServerOAuthConfigRuntime()); const userInfoResponse = await oauthClient.getUserInfo(accessToken); if (!userInfoResponse || !userInfoResponse.success) { diff --git a/app/routes/api.pdf-proxy.tsx b/app/routes/api.pdf-proxy.tsx new file mode 100644 index 0000000..3755cc7 --- /dev/null +++ b/app/routes/api.pdf-proxy.tsx @@ -0,0 +1,55 @@ +import { type LoaderFunctionArgs } from "@remix-run/node"; +import { getUserSession } from "~/api/login/auth.server"; +import { DOCUMENT_URL } from "~/api/axios-client"; + +/** + * PDF 代理路由 + * 用于在服务器端添加 JWT 认证后获取 PDF 文件 + */ +export async function loader({ request }: LoaderFunctionArgs) { + try { + // 获取 JWT token + const { frontendJWT } = await getUserSession(request); + + // 从查询参数获取文件路径 + const url = new URL(request.url); + const filePath = url.searchParams.get("path"); + + if (!filePath) { + return new Response("缺少文件路径参数", { status: 400 }); + } + + // 构建完整的文件 URL + const fileUrl = `${DOCUMENT_URL}${filePath}`; + + // 使用 JWT 认证获取文件 + const response = await fetch(fileUrl, { + headers: { + 'Authorization': `Bearer ${frontendJWT}`, + }, + }); + + if (!response.ok) { + return new Response(`获取文件失败: ${response.statusText}`, { + status: response.status + }); + } + + // 获取文件内容 + const blob = await response.blob(); + + // 返回文件,保持原始的 Content-Type + return new Response(blob, { + headers: { + 'Content-Type': response.headers.get('Content-Type') || 'application/pdf', + 'Cache-Control': 'public, max-age=3600', // 缓存1小时 + }, + }); + + } catch (error) { + console.error('PDF 代理错误:', error); + return new Response(`服务器错误: ${error instanceof Error ? error.message : '未知错误'}`, { + status: 500 + }); + } +} diff --git a/app/routes/callback.tsx b/app/routes/callback.tsx index ca842ae..8e43277 100644 --- a/app/routes/callback.tsx +++ b/app/routes/callback.tsx @@ -1,7 +1,21 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/node"; -import { createUserSession, saveUserInfo } from "~/api/login/auth.server"; +import { createUserSession, saveUserInfo, sessionStorage } from "~/api/login/auth.server"; import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt"; +/** + * 辅助函数:使用 session flash 重定向到登录页面并传递错误信息 + */ +async function redirectToLoginWithError(request: Request, errorMessage: string) { + const session = await sessionStorage.getSession(request.headers.get("Cookie")); + session.flash("loginError", errorMessage); + + return redirect("/login", { + headers: { + "Set-Cookie": await sessionStorage.commitSession(session) + } + }); +} + export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const origin = url.origin; // 获取请求的源 (e.g., "http://10.79.97.17:51703") @@ -21,19 +35,19 @@ export async function loader({ request }: LoaderFunctionArgs) { // 检查是否有错误 if (error) { console.error("❌ OAuth2.0授权失败:", error, error_description); - return redirect(`/login?error=${encodeURIComponent(error_description || error)}`); + return redirectToLoginWithError(request, error_description || error || "授权失败"); } // 检查是否有授权码 if (!code) { console.error("❌ OAuth2.0回调缺少授权码"); - return redirect("/login?error=missing_code"); + return redirectToLoginWithError(request, "登录过程中缺少授权码,请重新登录"); } // 验证状态值 if (!state || !state.endsWith("_idp")) { console.error("❌ OAuth2.0状态值验证失败:", { state, expectedSuffix: "_idp" }); - return redirect("/login?error=invalid_state"); + return redirectToLoginWithError(request, "登录状态验证失败,请重新登录"); } console.log("✅ OAuth2.0回调参数验证通过"); @@ -57,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) { if (!proxyResponse.ok || !tokenResponse.success) { console.error("❌ [Callback] 通过内部代理获取访问令牌失败:", tokenResponse); - return redirect("/login?error=token_proxy_error"); + return redirectToLoginWithError(request, "获取访问令牌失败,请重新登录"); } // --- 修改结束 --- @@ -78,7 +92,7 @@ export async function loader({ request }: LoaderFunctionArgs) { if (!userInfoProxyResponse.ok || !userInfoResponse.success) { console.error("❌ [Callback] 通过内部代理获取用户信息失败:", userInfoResponse); - return redirect("/login?error=userinfo_proxy_error"); + return redirectToLoginWithError(request, "获取用户信息失败,请重新登录"); } // 将代理返回的用户信息包装成与原有一致的结构 @@ -95,19 +109,34 @@ export async function loader({ request }: LoaderFunctionArgs) { // 获取重定向URL const redirectTo = url.searchParams.get("redirect") || "/"; - + + // 先生成一个临时 JWT + const tempUserInfo = { + sub: userInfo.data.sub, + user_id: userInfo.data.user_id || "", + username: userInfo.data.username, + nick_name: userInfo.data.nick_name, + email: userInfo.data.email, + phone_number: userInfo.data.phone_number, + ou_id: userInfo.data.ou_id, + ou_name: userInfo.data.ou_name, + is_leader: userInfo.data.is_leader, + user_role: userRole as 'common' | 'developer' + }; + const tempToken = JWTUtils.generateJWT(tempUserInfo, tokenResponse.expires_in); + // 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据 - const saveResult = await saveUserInfo(userInfo.data); + const saveResult = await saveUserInfo(userInfo.data, tempToken); if (!saveResult.success) { console.error("保存用户信息到数据库失败:", saveResult.error); // 注意:即使保存到数据库失败,我们仍然继续登录流程,因为用户已经通过了身份验证 - return redirect("/login?error=save_user_error"); + return redirectToLoginWithError(request, "保存用户信息失败,请重新登录"); } - + console.log("用户信息已成功保存到数据库"); const savedUserData = saveResult.data!; - - // 生成前端专用JWT + + // 生成前端专用JWT(使用完整的用户信息,包括数据库 ID) const jwtUserInfo: UserInfoForJWT = { sub: userInfo.data.sub, user_id: savedUserData.id!, @@ -120,7 +149,7 @@ export async function loader({ request }: LoaderFunctionArgs) { is_leader: savedUserData.is_leader, user_role: userRole as 'common' | 'developer' }; - + const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in); console.log("前端JWT已生成"); @@ -143,7 +172,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 使用统一的session创建函数 return createUserSession({ isAuthenticated: true, - userRole: userRole as 'common' | 'developer', + userRole: userRole, redirectTo, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token, @@ -154,7 +183,7 @@ export async function loader({ request }: LoaderFunctionArgs) { } catch (error) { console.error("OAuth2.0回调处理失败:", error); - return redirect("/login?error=callback_error"); + return redirectToLoginWithError(request, "登录回调处理失败,请重新登录"); } } diff --git a/app/routes/config-lists._index.tsx b/app/routes/config-lists._index.tsx index fef03c8..236c181 100644 --- a/app/routes/config-lists._index.tsx +++ b/app/routes/config-lists._index.tsx @@ -12,6 +12,7 @@ import { getConfigLists, getConfigOptions, updateConfigStatus, type ConfigItem } import configListsStyles from "~/styles/pages/config-lists_index.css?url"; import { toastService } from "~/components/ui/Toast"; import { messageService } from "~/components/ui/MessageModal"; +import { getUserSession } from "~/api/login/auth.server"; export const links = () => [ { rel: "stylesheet", href: configListsStyles } @@ -72,7 +73,10 @@ export async function loader({ request }: LoaderFunctionArgs) { const is_active = url.searchParams.get("is_active") ? url.searchParams.get("is_active") === "true" : undefined; const currentPage = parseInt(url.searchParams.get("page") || "1", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); - + + // 获取JWT token + const { frontendJWT } = await getUserSession(request); + try { // 获取配置列表 const configsResponse = await getConfigLists({ @@ -82,14 +86,14 @@ export async function loader({ request }: LoaderFunctionArgs) { is_active, page: currentPage, pageSize - }); + }, frontendJWT); if (configsResponse.error || !configsResponse.data) { throw new Error(configsResponse.error || "获取配置列表失败"); } // 获取配置选项 - const optionsResponse = await getConfigOptions(); + const optionsResponse = await getConfigOptions(frontendJWT); if (optionsResponse.error || !optionsResponse.data) { throw new Error(optionsResponse.error || "获取配置选项失败"); @@ -121,17 +125,20 @@ export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const _action = formData.get('_action'); const configId = formData.get('configId'); - + if (!configId) { return Response.json({ result: false, message: "缺少配置ID" }, { status: 400 }); } + // 获取JWT token + const { frontendJWT } = await getUserSession(request); + // 进行更新启用和禁用的状态 try { if (_action === 'toggleStatus') { const is_active = formData.get('is_active') === 'true'; - - const response = await updateConfigStatus(parseInt(configId as string), is_active); + + const response = await updateConfigStatus(parseInt(configId as string), is_active, frontendJWT); if (response.error) { return Response.json({ result: false, message: response.error }, { status: 500 }); diff --git a/app/routes/config-lists.new.tsx b/app/routes/config-lists.new.tsx index 2599c34..33be751 100644 --- a/app/routes/config-lists.new.tsx +++ b/app/routes/config-lists.new.tsx @@ -7,6 +7,7 @@ import { ENVIRONMENT_LABELS } from "./config-lists._index"; import { getConfigOptions, getConfigDetail, createConfig, updateConfig } from "~/api/system_setting/config-lists"; import configNewStyles from "~/styles/pages/config-lists_new.css?url"; import { toastService } from "~/components/ui/Toast"; +import { getUserSession } from "~/api/login/auth.server"; export const links = () => [ { rel: "stylesheet", href: configNewStyles } @@ -113,17 +114,20 @@ export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const id = url.searchParams.get("id"); let config: ConfigData | undefined = undefined; - + + // 获取JWT token + const { frontendJWT } = await getUserSession(request); + try { // 获取配置选项 - const optionsResponse = await getConfigOptions(); + const optionsResponse = await getConfigOptions(frontendJWT); if (optionsResponse.error) { throw new Error(optionsResponse.error); } - + if (id) { // 获取配置详情 - const detailResponse = await getConfigDetail(id); + const detailResponse = await getConfigDetail(id, frontendJWT); if (detailResponse.error) { throw new Error(detailResponse.error); } @@ -159,7 +163,10 @@ export async function action({ request }: ActionFunctionArgs) { const config = formData.get("config") as string; const is_active = formData.get("is_active") === "true"; const remark = formData.get("remark") as string; - + + // 获取JWT token + const { frontendJWT } = await getUserSession(request); + const errors: ActionData["errors"] = {}; // 表单验证 @@ -206,13 +213,13 @@ export async function action({ request }: ActionFunctionArgs) { if (id) { // 更新配置 - const response = await updateConfig(id, configData); + const response = await updateConfig(id, configData, frontendJWT); if (response.error) { throw new Error(response.error); } } else { // 创建配置 - const response = await createConfig(configData); + const response = await createConfig(configData, frontendJWT); if (response.error) { throw new Error(response.error); } diff --git a/app/routes/contract-template.detail.$id.tsx b/app/routes/contract-template.detail.$id.tsx index e3bf70e..9e26834 100644 --- a/app/routes/contract-template.detail.$id.tsx +++ b/app/routes/contract-template.detail.$id.tsx @@ -4,6 +4,7 @@ import { getContractTemplate } from '~/api/contract-template/templates'; import type { ContractTemplate } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url'; +import { getUserSession } from '~/api/login/auth.server'; // 导入FilePreview组件 import { FilePreview } from '~/components/reviews'; @@ -30,20 +31,24 @@ export const handle = { } }; -export async function loader({ params }: LoaderFunctionArgs) { +export async function loader({ params, request }: LoaderFunctionArgs) { const templateId = params.id!; + // 获取 JWT + const { frontendJWT } = await getUserSession(request); + const jwt = frontendJWT || undefined; + try { - const response = await getContractTemplate(templateId); - - if (response.error) { - throw new Response(response.error, { status: response.status || 404 }); - } - - if (!response.data) { - throw new Response('模板未找到', { status: 404 }); - } - + const response = await getContractTemplate(templateId, jwt); + + if (response.error) { + throw new Response(response.error, { status: response.status || 404 }); + } + + if (!response.data) { + throw new Response('模板未找到', { status: 404 }); + } + // 添加调试信息 // console.log('模板详情数据:', response.data); // console.log('分类信息:', response.data.category); diff --git a/app/routes/contract-template.list._index.tsx b/app/routes/contract-template.list._index.tsx index c9cdc35..fc897f2 100644 --- a/app/routes/contract-template.list._index.tsx +++ b/app/routes/contract-template.list._index.tsx @@ -7,6 +7,7 @@ import { Pagination } from '~/components/ui/Pagination'; import { getContractTemplates, getContractCategoriesWithCount } from '~/api/contract-template/templates'; import type { ContractTemplate, TemplateSearchParams, ContractCategoryWithCount } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; +import { getUserSession } from '~/api/login/auth.server'; export const links = () => [ { rel: 'stylesheet', href: styles } @@ -47,91 +48,95 @@ export async function loader({ request }: LoaderFunctionArgs) { const page = parseInt(url.searchParams.get('page') || '1'); const pageSize = 12 + // 获取 JWT + const { frontendJWT } = await getUserSession(request); + const jwt = frontendJWT || undefined; + try { // 根据sortBy值设置数据库排序参数 let dbSortBy = 'id'; let dbSortOrder: 'asc' | 'desc' = 'asc'; - - switch (sortBy) { - case 'relevance': - dbSortBy = 'id'; - dbSortOrder = 'asc'; - break; - case 'newest': - dbSortBy = 'updated_at'; - dbSortOrder = 'desc'; - break; - /* case 'popular': - // 暂时按创建时间排序,后续可以加入使用频率字段 - dbSortBy = 'created_at'; - dbSortOrder = 'desc'; - break; - case 'rating': - // 暂时按特色推荐排序,后续可以加入评分字段 - dbSortBy = 'is_featured'; - dbSortOrder = 'desc'; - break; */ - default: - dbSortBy = 'id'; - dbSortOrder = 'asc'; - } + + switch (sortBy) { + case 'relevance': + dbSortBy = 'id'; + dbSortOrder = 'asc'; + break; + case 'newest': + dbSortBy = 'updated_at'; + dbSortOrder = 'desc'; + break; + /* case 'popular': + // 暂时按创建时间排序,后续可以加入使用频率字段 + dbSortBy = 'created_at'; + dbSortOrder = 'desc'; + break; + case 'rating': + // 暂时按特色推荐排序,后续可以加入评分字段 + dbSortBy = 'is_featured'; + dbSortOrder = 'desc'; + break; */ + default: + dbSortBy = 'id'; + dbSortOrder = 'asc'; + } - // 构建搜索参数 - const searchParams: TemplateSearchParams = { - page, - pageSize, - sortBy: dbSortBy, - sortOrder: dbSortOrder - }; + // 构建搜索参数 + const searchParams: TemplateSearchParams = { + page, + pageSize, + sortBy: dbSortBy, + sortOrder: dbSortOrder + }; - // 优先使用category_id,其次使用category名称 - if (category_id) { - searchParams.category_id = parseInt(category_id); - } else if (category) { - searchParams.category = category; - } + // 优先使用category_id,其次使用category名称 + if (category_id) { + searchParams.category_id = parseInt(category_id); + } else if (category) { + searchParams.category = category; + } // 并行获取模板数据和分类数据 const [templatesResponse, categoriesResponse] = await Promise.all([ - getContractTemplates(searchParams), - getContractCategoriesWithCount() + getContractTemplates({ ...searchParams, token: jwt }), + getContractCategoriesWithCount(jwt) ]); - // 处理模板数据 - if (templatesResponse.error) { - console.error('获取模板列表失败:', templatesResponse.error); - return { - templates: [], - total: 0, - page, - pageSize, - category, - category_id, - type, - sortBy, - categories: [] - }; - } + // 处理模板数据 + if (templatesResponse.error) { + console.error('获取模板列表失败:', templatesResponse.error); + return { + templates: [], + total: 0, + page, + pageSize, + category, + category_id, + type, + sortBy, + categories: [] + }; + } - // 处理分类数据 - const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || []; + // 处理分类数据 + const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || []; - // 转换模板数据格式 - const transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || []; + // 转换模板数据格式 + const transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || []; - // 注释掉类型筛选,因为数据库中没有type字段且已隐藏该功能 - /* if (type) { - transformedTemplates = transformedTemplates.filter(t => t.type === type); - } */ + // 注释掉类型筛选,因为数据库中没有type字段且已隐藏该功能 + /* if (type) { + transformedTemplates = transformedTemplates.filter(t => t.type === type); + } */ - // 获取当前分类信息(用于显示) - let currentCategory = '全部'; - if (category_id) { - const cat = categories.find(c => c.id === parseInt(category_id)); - currentCategory = cat?.name || '全部'; - } else if (category) { - currentCategory = category; - } + // 获取当前分类信息(用于显示) + let currentCategory = '全部'; + if (category_id) { + const cat = categories.find(c => c.id === parseInt(category_id)); + currentCategory = cat?.name || '全部'; + } else if (category) { + currentCategory = category; + } return { templates: transformedTemplates, diff --git a/app/routes/contract-template.search._index.tsx b/app/routes/contract-template.search._index.tsx index 876eb2a..5229fc6 100644 --- a/app/routes/contract-template.search._index.tsx +++ b/app/routes/contract-template.search._index.tsx @@ -1,9 +1,10 @@ -import type { MetaFunction } from '@remix-run/node'; +import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node'; import { useNavigate, useLoaderData } from '@remix-run/react'; import { ContractSearchHero } from '~/components/contract-template/ContractSearchHero'; import { getContractCategoriesWithCount } from '~/api/contract-template/templates'; import type { ContractCategoryWithCount } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; +import { getUserSession } from '~/api/login/auth.server'; export const links = () => [ { rel: 'stylesheet', href: styles } @@ -42,18 +43,22 @@ function transformCategory(category: ContractCategoryWithCount) { * 加载分类数据 * @returns 分类数据 */ -export async function loader() { +export async function loader({ request }: LoaderFunctionArgs) { + // 获取 JWT + const { frontendJWT } = await getUserSession(request); + const jwt = frontendJWT || undefined; + try { // 使用聚合查询获取分类及其模板数量 - const categoriesResponse = await getContractCategoriesWithCount(); + const categoriesResponse = await getContractCategoriesWithCount(jwt); - // 处理分类数据 - if (categoriesResponse.error) { - console.error('获取分类失败:', categoriesResponse.error); - return { categories: [] }; - } + // 处理分类数据 + if (categoriesResponse.error) { + console.error('获取分类失败:', categoriesResponse.error); + return { categories: [] }; + } - const categories = categoriesResponse.data || []; + const categories = categoriesResponse.data || []; // 转换分类数据格式 const categoriesWithCount = categories.map(transformCategory); diff --git a/app/routes/contract-template.search.results.tsx b/app/routes/contract-template.search.results.tsx index 4b37c2e..64d0286 100644 --- a/app/routes/contract-template.search.results.tsx +++ b/app/routes/contract-template.search.results.tsx @@ -9,6 +9,7 @@ import { Pagination } from '~/components/ui/Pagination'; import { searchContractTemplates, getContractCategories } from '~/api/contract-template/templates'; import type { ContractTemplate, ContractCategory } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; +import { getUserSession } from '~/api/login/auth.server'; export const links = () => [ { rel: 'stylesheet', href: styles } @@ -105,6 +106,10 @@ export async function loader({ request }: LoaderFunctionArgs) { dbSortOrder = 'asc'; } + // 获取 JWT + const { frontendJWT } = await getUserSession(request); + const jwt = frontendJWT || undefined; + try { // 并行获取搜索结果和分类数据 const [searchResponse, categoriesResponse] = await Promise.all([ @@ -113,67 +118,69 @@ export async function loader({ request }: LoaderFunctionArgs) { page, pageSize, sortBy: dbSortBy, - sortOrder: dbSortOrder + sortOrder: dbSortOrder, + token: jwt }), - getContractCategories() + getContractCategories(jwt) ]); - // 处理搜索结果 - if (searchResponse.error) { - console.error('搜索合同模板失败:', searchResponse.error); - return { - results: [], - query, - category, - total: 0, - page, - pageSize, - sortBy, - searchTime: '搜索失败', - categories: [] - }; - } + // 处理搜索结果 + if (searchResponse.error) { + console.error('搜索合同模板失败:', searchResponse.error); + return { + results: [], + query, + category, + total: 0, + page, + pageSize, + sortBy, + searchTime: '搜索失败', + categories: [] + }; + } - // 处理分类数据 - const categories = categoriesResponse.error ? [] : categoriesResponse.data || []; + // 处理分类数据 + const categories = categoriesResponse.error ? [] : categoriesResponse.data || []; - // 转换模板数据格式 - const transformedResults = searchResponse.data?.templates.map(transformTemplate) || []; + // 转换模板数据格式 + const transformedResults = searchResponse.data?.templates.map(transformTemplate) || []; - // 为每个分类获取搜索结果统计 - let categoriesWithSearchCount: CategoryWithSearchCount[] = []; - if (query && query.trim()) { - // 并行为每个分类获取搜索结果数量 - const categorySearchPromises = categories.map(async (cat): Promise => { - try { - const categorySearchResponse = await searchContractTemplates(query, { - category: cat.name, - page: 1, - pageSize: 1000 // 设置较大的pageSize来获取总数 - }); - - const count = categorySearchResponse.error ? 0 : (categorySearchResponse.data?.total || 0); - return { - ...cat, - searchCount: count - }; - } catch (error) { - console.error(`获取分类${cat.name}的搜索统计失败:`, error); - return { - ...cat, - searchCount: 0 - }; - } - }); + // 为每个分类获取搜索结果统计 + let categoriesWithSearchCount: CategoryWithSearchCount[] = []; + if (query && query.trim()) { + // 并行为每个分类获取搜索结果数量 + const categorySearchPromises = categories.map(async (cat): Promise => { + try { + const categorySearchResponse = await searchContractTemplates(query, { + category: cat.name, + page: 1, + pageSize: 1000, // 设置较大的pageSize来获取总数 + token: jwt + }); + + const count = categorySearchResponse.error ? 0 : (categorySearchResponse.data?.total || 0); + return { + ...cat, + searchCount: count + }; + } catch (error) { + console.error(`获取分类${cat.name}的搜索统计失败:`, error); + return { + ...cat, + searchCount: 0 + }; + } + }); - categoriesWithSearchCount = await Promise.all(categorySearchPromises); - } else { - // 如果没有搜索关键词,searchCount设为0 - categoriesWithSearchCount = categories.map(cat => ({ - ...cat, - searchCount: 0 - })); - } + categoriesWithSearchCount = await Promise.all(categorySearchPromises); + } else { + // 如果没有搜索关键词,searchCount设为0 + categoriesWithSearchCount = categories.map(cat => ({ + ...cat, + searchCount: 0 + })); + } // 计算搜索耗时 const endTime = Date.now(); diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index f7ee4d8..a553ef2 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -50,6 +50,7 @@ export type LoaderData = { completedTasks: number; }; initialLoad?: boolean; + frontendJWT?: string; // 新增JWT }; export async function loader({ request }: LoaderFunctionArgs) { @@ -100,7 +101,8 @@ export async function loader({ request }: LoaderFunctionArgs) { currentPage: tasksResponse.data?.currentPage || params.page, pageSize: tasksResponse.data?.pageSize || params.pageSize, totalPages: tasksResponse.data?.totalPages || 0, - stats: statsResponse.data || { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 } + stats: statsResponse.data || { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 }, + frontendJWT // 新增:返回JWT给客户端 }, { headers: { "Cache-Control": "max-age=60, s-maxage=180" @@ -210,7 +212,7 @@ const docTypeConfig = { export default function CrossCheckingIndex() { const loaderData = useLoaderData(); - const { tasks, totalCount, currentPage, pageSize, stats } = loaderData; + const { tasks, totalCount, currentPage, pageSize, stats, frontendJWT } = loaderData; const [searchParams, setSearchParams] = useSearchParams(); const dateFrom = searchParams.get('dateFrom') || ''; const dateTo = searchParams.get('dateTo') || ''; @@ -750,6 +752,7 @@ export default function CrossCheckingIndex() { total={modalState.total} onPageChange={handleModalPageChange} onPageSizeChange={handleModalPageSizeChange} + frontendJWT={frontendJWT} />
); diff --git a/app/routes/cross-checking.result.tsx b/app/routes/cross-checking.result.tsx index c226009..4852aa7 100644 --- a/app/routes/cross-checking.result.tsx +++ b/app/routes/cross-checking.result.tsx @@ -202,7 +202,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const reviewData = await getReviewPoints(id, request); // 获取当前登录用户是否是发起人 - const isProposer = await findIsProposer(taskId, userInfo?.user_id); + const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT); // console.log("documentData-------",JSON.stringify(documentData.data,null,2)); // console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2)); @@ -560,9 +560,9 @@ export default function CrossCheckingResult() { cancelText: '取消', onConfirm: async () => { setIsLoading(true); - const res = await confirmReviewResults(document.id); + const res = await confirmReviewResults(document.id, jwtToken); setIsLoading(false); - + if (res.error) { toastService.error(res.error); return; diff --git a/app/routes/document-types._index.tsx b/app/routes/document-types._index.tsx index 886e968..386a81a 100644 --- a/app/routes/document-types._index.tsx +++ b/app/routes/document-types._index.tsx @@ -42,11 +42,16 @@ interface LoaderData { error?: string; groups: DocumentTypeGroup[]; ruleTypes: RuleType[]; + frontendJWT?: string | null; } // 加载函数 - 获取文档类型列表 export async function loader({ request }: LoaderFunctionArgs) { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const url = new URL(request.url); const name = url.searchParams.get('name') || undefined; const ruleType = url.searchParams.get('ruleType') || undefined; @@ -64,13 +69,13 @@ export async function loader({ request }: LoaderFunctionArgs) { }; // 并行获取文档类型数据和父级评查点分组 - const ruleTypesResponse = await getRuleTypes(); + const ruleTypesResponse = await getRuleTypes(undefined, frontendJWT); if(ruleTypesResponse.error){ console.error("获取父级评查点分组失败:", ruleTypesResponse.error); } const ruleTypes = ruleTypesResponse.error ? [] : ruleTypesResponse.data; - const typesResponse = await getDocumentTypes(searchParams); + const typesResponse = await getDocumentTypes(searchParams, frontendJWT); if(typesResponse.error){ console.error("获取文档类型失败:", typesResponse.error); throw new Error(typesResponse.error); @@ -85,7 +90,8 @@ export async function loader({ request }: LoaderFunctionArgs) { total: typesResponse.data?.total || typesResult.length, pageSize, currentPage: page, - ruleTypes + ruleTypes, + frontendJWT }); } catch (error) { console.error("加载文档类型列表失败:", error); @@ -104,10 +110,12 @@ export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const id = formData.get("id") as string; const intent = formData.get("intent") as string; + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); if (intent === "delete" && id) { try { - const result = await deleteDocumentType(id); + const result = await deleteDocumentType(id, frontendJWT || undefined); if (result.error) { return Response.json({ success: false, error: result.error }, { status: result.status || 500 }); @@ -132,7 +140,7 @@ export default function DocumentTypesList() { const [isDeleting, setIsDeleting] = useState(false); // 获取加载器数据 - const { types, total, error, ruleTypes } = useLoaderData(); + const { types, total, error, ruleTypes, frontendJWT } = useLoaderData(); // 状态管理 const [ruleGroups, setRuleGroups] = useState([]); @@ -160,7 +168,7 @@ export default function DocumentTypesList() { const loadRuleGroups = async () => { setLoadingGroups(true); try { - const response = await getRuleGroupsByType(ruleTypeParam); + const response = await getRuleGroupsByType(ruleTypeParam, frontendJWT || undefined); if (response.data) { setRuleGroups(response.data); } else if (response.error) { diff --git a/app/routes/document-types.new.tsx b/app/routes/document-types.new.tsx index d7ccd52..8d376b5 100644 --- a/app/routes/document-types.new.tsx +++ b/app/routes/document-types.new.tsx @@ -62,12 +62,16 @@ interface ActionData { // 获取数据 export async function loader({ request }: LoaderFunctionArgs) { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const url = new URL(request.url); const id = url.searchParams.get("id"); const isEdit = id ? true : false; // 1. 获取评查点分组 - 使用getAllRuleGroups获取所有分组 - const ruleGroupsResponse = await getAllRuleGroups(); + const ruleGroupsResponse = await getAllRuleGroups(frontendJWT); if (ruleGroupsResponse.error) { console.error("获取评查点分组失败:", ruleGroupsResponse.error); // throw new Error(ruleGroupsResponse.error); @@ -80,16 +84,16 @@ export async function loader({ request }: LoaderFunctionArgs) { // 2. 获取各类型的提示词模板 const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] = await Promise.all([ - getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }), - getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }), - getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }), - getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY }) + getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }, frontendJWT), + getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }, frontendJWT), + getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }, frontendJWT), + getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY }, frontendJWT) ]); // 3. 如果是编辑模式,获取文档类型详情 let documentType = undefined; if (id) { - const typeResponse = await getDocumentType(id); + const typeResponse = await getDocumentType(id, frontendJWT); if (typeResponse.data) { documentType = typeResponse.data; } @@ -121,6 +125,9 @@ export async function loader({ request }: LoaderFunctionArgs) { // 处理表单提交 export async function action({ request }: ActionFunctionArgs) { + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const formData = await request.formData(); const id = formData.get("id") as string | null; const name = formData.get("name") as string; @@ -185,11 +192,11 @@ export async function action({ request }: ActionFunctionArgs) { // 更新文档类型 response = await updateDocumentType(id, { ...documentTypeData, - id: parseInt(id) - }); + id: parseInt(id), + }, frontendJWT); } else { // 创建新文档类型 - response = await createDocumentType(documentTypeData); + response = await createDocumentType(documentTypeData, frontendJWT); } if (response.error) { diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 2ecf9ca..9bbf5b5 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -46,7 +46,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); // 获取文档类型列表,用于筛选条件 - const typesResponse = await getDocumentTypes({ pageSize: 500 }); + const typesResponse = await getDocumentTypes({ pageSize: 500 }, frontendJWT); const documentTypes = typesResponse.data?.types || []; const documentTypeOptions = documentTypes.map(type => ({ value: type.id, @@ -126,7 +126,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { try { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); - const { userInfo } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { return Response.json({ result: false, message: "用户身份验证失败" }, { status: 401 }); @@ -138,7 +138,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { if (action === "delete") { const id = formData.get("id") as string; - const response = await deleteDocument(id, userId); + const response = await deleteDocument(id, userId, frontendJWT); if (response.error) { return Response.json({ result: false, message: response.error }, { status: response.status || 500 }); @@ -150,7 +150,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const ids = formData.getAll("ids") as string[]; // 批量删除处理 - const results = await Promise.all(ids.map(id => deleteDocument(id, userId))); + const results = await Promise.all(ids.map(id => deleteDocument(id, userId, frontendJWT))); const failures = results.filter(r => r.error); if (failures.length > 0) { @@ -257,7 +257,8 @@ export default function DocumentsIndex() { reviewType: storedReviewType || undefined, userId: userId, // 添加用户ID筛选 page: currentPage, - pageSize + pageSize, + token: loaderData.frontendJWT || undefined // 传递 JWT token }; // 获取文档列表 @@ -270,7 +271,7 @@ export default function DocumentsIndex() { const filteredTypesResponse = await getDocumentTypes({ pageSize: 500, reviewType: storedReviewType || undefined - }); + }, loaderData.frontendJWT || undefined); const filteredDocumentTypes = filteredTypesResponse.data?.types || []; const filteredOptions = filteredDocumentTypes.map(type => ({ value: type.id, @@ -492,20 +493,21 @@ export default function DocumentsIndex() { // 下载文档 const handleDownload = async (path: string) => { try { - const downloadUrl = `${DOCUMENT_URL}${path}`; - + // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 + const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`; + // 使用fetch获取文件内容 const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } - + // 将响应转换为Blob const blob = await response.blob(); - + // 创建Blob URL const blobUrl = URL.createObjectURL(blob); - + // 创建一个隐藏的a标签并点击它 const a = document.createElement('a'); a.style.display = 'none'; @@ -515,7 +517,7 @@ export default function DocumentsIndex() { a.download = decodeURIComponent(fileName); document.body.appendChild(a); a.click(); - + // 清理 setTimeout(() => { document.body.removeChild(a); @@ -631,24 +633,25 @@ export default function DocumentsIndex() { console.warn(`文档 ${doc.name} 没有有效的路径`); return; } - - const downloadUrl = `${DOCUMENT_URL}${doc.path}`; - + + // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 + const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(doc.path)}`; + // 获取文件内容 const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } - + // 将响应转换为Blob const blob = await response.blob(); - + // 从路径中获取文件名 const fileName = doc.path.split('/').pop() || doc.name; - + // 添加到ZIP文件 zip.file(decodeURIComponent(fileName), blob); - + return { success: true, name: fileName }; } catch (error) { console.error(`下载文件 ${doc.name} 失败:`, error); @@ -714,7 +717,7 @@ export default function DocumentsIndex() { } // console.log('开始审核',fileId,auditStatus) - const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId); + const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId, loaderData.frontendJWT as string | undefined); if (response.error) { console.error('更新文件审核状态失败:', response.error); toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); diff --git a/app/routes/documents.download.tsx b/app/routes/documents.download.tsx index e8c9dac..44ff67a 100644 --- a/app/routes/documents.download.tsx +++ b/app/routes/documents.download.tsx @@ -1,5 +1,6 @@ import { LoaderFunctionArgs } from "@remix-run/node"; import { postgrestGet } from "~/api/postgrest-client"; +import { getUserSession } from "~/api/login/auth.server"; /** * 文档下载路由 - 处理文档下载请求 @@ -7,6 +8,7 @@ import { postgrestGet } from "~/api/postgrest-client"; */ export async function loader({ request }: LoaderFunctionArgs) { try { + const { frontendJWT } = await getUserSession(request); // 获取文件路径参数 const url = new URL(request.url); const filePath = url.searchParams.get("path"); @@ -23,7 +25,8 @@ export async function loader({ request }: LoaderFunctionArgs) { filter: { 'object_path': `eq.${filePath}`, 'expires_in': 'eq.300' // 5分钟有效期 - } + }, + token: frontendJWT } ); diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index 3d1efa4..e3a286b 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -82,7 +82,7 @@ export async function loader({ request }: LoaderFunctionArgs) { try { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); - const { userInfo } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { throw new Response("用户身份验证失败", { status: 401 }); @@ -100,8 +100,8 @@ export async function loader({ request }: LoaderFunctionArgs) { // 并行获取文档详情和文档类型列表 const [documentResponse, documentTypesResponse] = await Promise.all([ - getDocument(id, userId), - getDocumentTypes({ pageSize: 500 }) + getDocument(id, userId, frontendJWT), + getDocumentTypes({ pageSize: 500 }, frontendJWT) ]); if (documentResponse.error) { @@ -114,7 +114,8 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ document: documentResponse.data, - documentTypes: documentTypesResponse.data?.types || [] + documentTypes: documentTypesResponse.data?.types || [], + frontendJWT: frontendJWT }); } catch (error) { console.error("加载文档数据失败:", error); @@ -126,7 +127,7 @@ export async function loader({ request }: LoaderFunctionArgs) { export async function action({ request }: ActionFunctionArgs) { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); - const { userInfo } = await getUserSession(request); + const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { return Response.json({ error: "用户身份验证失败" }, { status: 401 }); @@ -173,7 +174,7 @@ export async function action({ request }: ActionFunctionArgs) { auditStatus, isTest, remark - }, userId); + }, userId, frontendJWT); if (updateResponse.error) { console.error('更新文档失败:', updateResponse.error); @@ -323,7 +324,7 @@ export default function DocumentEdit() { return (
{ console.error("PDF加载错误:", error); @@ -416,20 +417,21 @@ export default function DocumentEdit() { // 下载文档 const downloadDocument = async () => { try { - const downloadUrl = `${DOCUMENT_URL}${documentData.path}`; - + // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 + const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`; + // 使用fetch获取文件内容 const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } - + // 将响应转换为Blob const blob = await response.blob(); - + // 创建Blob URL const blobUrl = URL.createObjectURL(blob); - + // 创建一个隐藏的a标签并点击它 const a = document.createElement('a'); a.style.display = 'none'; @@ -439,23 +441,24 @@ export default function DocumentEdit() { a.download = decodeURIComponent(fileName); document.body.appendChild(a); a.click(); - + // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 100); - + toastService.success('文件下载成功'); } catch (error) { console.error('下载文件失败:', error); toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; - + // 在新窗口打开文档预览 const openPreview = () => { - const previewUrl = `${DOCUMENT_URL}${documentData.path}`; + // 使用 PDF 代理路由,自动添加 JWT 认证 + const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`; window.open(previewUrl, '_blank'); }; diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 2eb92f8..be370a4 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -245,8 +245,8 @@ export async function loader({ request }: LoaderFunctionArgs) { // 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤 // 并行加载文档和文档类型 const [documentsResponse, typesResponse] = await Promise.all([ - getTodayDocuments(userInfo), - getDocumentTypes() + getTodayDocuments(userInfo, undefined, frontendJWT), + getDocumentTypes(undefined, frontendJWT) ]); // console.log('loader: 文档加载结果:', documentsResponse); @@ -410,7 +410,7 @@ export default function FilesUpload() { try { // 使用 reviewType 获取过滤后的文档列表 - const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType); + const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType, loaderData.frontendJWT || undefined); if (response.error) { console.error('过滤文档列表失败:', response.error); @@ -575,16 +575,16 @@ export default function FilesUpload() { } }); - console.log('合同主文件ID:', mainDocumentIds); - console.log('合同附件ID:', attachmentIds); + // console.log('合同主文件ID:', mainDocumentIds); + // console.log('合同附件ID:', attachmentIds); // 分别查询状态 - statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds); + statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds, loaderData.frontendJWT || undefined); } else { // 非合同类型,使用原有逻辑 const incompleteIds = incompleteFiles.map(file => file.id); // console.log('未完成的文档ID:', incompleteIds); - statusResponse = await getDocumentsStatus(incompleteIds); + statusResponse = await getDocumentsStatus(incompleteIds, undefined, loaderData.frontendJWT || undefined); } // console.log('状态检查响应:', statusResponse); @@ -1666,7 +1666,7 @@ export default function FilesUpload() { // 获取文件状态 // console.log('【调试-checkProcessingStatus】发送请求获取文件状态'); - const response = await getDocumentsStatus(fileIds); + const response = await getDocumentsStatus(fileIds, undefined, loaderData.frontendJWT || undefined); if (response.error) { console.error('【调试-checkProcessingStatus】获取文件状态出错:', response.error); diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 31173ec..dc5c5d5 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -92,7 +92,7 @@ export async function action({ request }: ActionFunctionArgs) { export default function Home() { const navigate = useNavigate(); - const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo } = useLoaderData(); + const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo, frontendJWT } = useLoaderData(); const [recentFiles, setRecentFiles] = useState(initialRecentFiles || []); const [homeData, setHomeData] = useState(initialHomeData); const [currentDateTime, setCurrentDateTime] = useState({ @@ -160,9 +160,9 @@ export default function Home() { setIsLoading(true); // 从 sessionStorage 获取 reviewType const reviewType = sessionStorage.getItem('reviewType'); - + // 加载主页数据 - const newHomeData = await getHomeData(reviewType || undefined,userInfo.user_id); + const newHomeData = await getHomeData(reviewType || undefined,userInfo.user_id, frontendJWT); setHomeData(newHomeData); // 加载文档数据 @@ -185,7 +185,8 @@ export default function Home() { const documentSearchParams: DocumentSearchParams = { page: 1, pageSize: 10, - userId: userInfo.user_id + userId: userInfo.user_id, + token: frontendJWT || undefined }; // 根据 reviewType 添加过滤条件 @@ -244,9 +245,9 @@ export default function Home() { // 如果 reviewType 发生变化 if (currentReviewType !== previousReviewType) { setIsLoading(true); - + // 更新主页数据 - const newHomeData = await getHomeData(currentReviewType || undefined,userInfo.user_id); + const newHomeData = await getHomeData(currentReviewType || undefined,userInfo.user_id, frontendJWT); setHomeData(newHomeData); // 更新文档数据 diff --git a/app/routes/login.tsx b/app/routes/login.tsx index fc2aafb..a20a68f 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useSearchParams, Form } from "@remix-run/react"; +import { useActionData, useLoaderData, Form } from "@remix-run/react"; import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; import { OAUTH_CONFIG } from "~/config/api-config"; @@ -32,11 +32,30 @@ export async function loader({ request }: LoaderFunctionArgs) { const redirectTo = url.searchParams.get("redirect") || "/"; const session = await getSession(request); + + // 读取 flash 消息(来自 callback 的错误) + const loginError = session.get("loginError"); + session.set("redirectTo", redirectTo); + // 提交 session 以清除 flash 消息 + if (loginError) { + const { sessionStorage } = await import("~/api/login/auth.server"); + return Response.json({ + isAuthenticated: false, + redirectTo, + flashError: loginError + }, { + headers: { + "Set-Cookie": await sessionStorage.commitSession(session) + } + }); + } + return Response.json({ isAuthenticated: false, - redirectTo + redirectTo, + flashError: null }); } @@ -60,10 +79,17 @@ export async function action({ request }: ActionFunctionArgs) { // 登录成功,直接返回重定向响应 return response; } else { - // 登录失败,解析错误信息并重定向到登录页面 + // 登录失败,返回错误信息(不再使用URL参数) const errorData = await response.json(); - const errorMsg = errorData.error || "登录失败"; - return redirect(`/login?error=${encodeURIComponent(errorMsg)}`); + return Response.json({ + success: false, + error: errorData.error || "登录失败", + retryCount: errorData.retryCount || 0, + isLocked: errorData.isLocked || false, + remainingAttempts: errorData.remainingAttempts || 5 + }, { + status: response.status + }); } } @@ -71,40 +97,19 @@ export async function action({ request }: ActionFunctionArgs) { } export default function Login() { - const [searchParams] = useSearchParams(); - const error = searchParams.get("error"); + const actionData = useActionData(); + const loaderData = useLoaderData(); const [isFlipped, setIsFlipped] = useState(false); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - // 获取错误消息的友好描述 - const getErrorMessage = (error: string | null) => { - if (!error) return null; - - switch (error) { - case "missing_code": - return "登录过程中缺少授权码,请重新登录"; - case "invalid_state": - return "登录状态验证失败,请重新登录"; - case "token_error": - return "获取访问令牌失败,请重新登录"; - case "userinfo_error": - return "获取用户信息失败,请重新登录"; - case "callback_error": - return "登录回调处理失败,请重新登录"; - case "用户名和密码不能为空": - case "用户名和密码不能为空,请重新输入": - return "用户名和密码不能为空,请重新输入"; - case "登录失败,请检查用户名和密码": - case "用户名或密码错误,请重新输入": - return "用户名或密码错误,请重新输入"; - case "登录请求失败,请稍后重试": - case "网络连接失败,请稍后重试": - return "网络连接失败,请稍后重试"; - default: - return decodeURIComponent(error); - } - }; + // 从 actionData 或 loaderData 中获取错误信息 + // actionData 的错误优先(来自密码登录) + // loaderData.flashError 次之(来自 OAuth 回调) + const error = actionData?.error || loaderData?.flashError; + const isLocked = actionData?.isLocked || false; + const retryCount = actionData?.retryCount || 0; + const remainingAttempts = actionData?.remainingAttempts || 5; // 处理OAuth2.0登录 const handleOAuthLogin = () => { @@ -143,6 +148,13 @@ export default function Login() { // 处理账号密码登录表单提交 const handlePasswordLoginSubmit = (e: React.FormEvent) => { + // 检查账户是否被锁定 + if (isLocked) { + e.preventDefault(); + toastService.error("账户已被锁定,请联系管理员"); + return; + } + // 客户端验证 if (!username.trim()) { e.preventDefault(); @@ -180,9 +192,22 @@ export default function Login() {

统一身份认证登录

{error && ( -
-
-
{getErrorMessage(error)}
+
+
+ {isLocked ? ( + + ) : ( + + )} +
+
+ {error} + {!isLocked && retryCount > 0 && ( +
+ 剩余尝试次数:{remainingAttempts} 次 +
+ )} +
)} @@ -235,9 +260,22 @@ export default function Login() {

管理员登录

{error && ( -
-
-
{getErrorMessage(error)}
+
+
+ {isLocked ? ( + + ) : ( + + )} +
+
+ {error} + {!isLocked && retryCount > 0 && ( +
+ 剩余尝试次数:{remainingAttempts} 次 +
+ )} +
)} @@ -275,10 +313,27 @@ export default function Login() { + + {isLocked && ( +
+ 账户已被锁定,请联系管理员解锁 +
+ )}
diff --git a/app/routes/prompts._index.tsx b/app/routes/prompts._index.tsx index 2d3f531..aa3c4d2 100644 --- a/app/routes/prompts._index.tsx +++ b/app/routes/prompts._index.tsx @@ -1,6 +1,6 @@ -import { MetaFunction, json, type LoaderFunctionArgs } from "@remix-run/node"; -import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react"; -import { useState } from "react"; +import { MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; +import { useSearchParams, useNavigate, useLoaderData, useFetcher } from "@remix-run/react"; +import { useState, useEffect } from "react"; import indexStyles from "~/styles/pages/prompts_index.css?url"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; @@ -9,19 +9,6 @@ import { Table } from "~/components/ui/Table"; import { Pagination } from "~/components/ui/Pagination"; import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts"; -// 定义提示词模板类型 -export interface PromptTemplate { - id: string; - template_name: string; - template_type: 'Common' | 'Extraction' | 'Evaluation' | 'Summary'; - description: string; - version: string; - status: 'active' | 'inactive' | 'system'; - created_by: string; - template_content: string; - variables: string; // JSON字符串 -} - // 样式链接 export function links() { return [{ rel: "stylesheet", href: indexStyles }]; @@ -44,18 +31,28 @@ interface LoaderData { error?: string; } +// 定义 Action 返回数据类型 +interface ActionData { + success: boolean; + error?: string; +} + // 数据加载器 export async function loader({ request }: LoaderFunctionArgs) { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const url = new URL(request.url); const name = url.searchParams.get('name') || undefined; const type = url.searchParams.get('type') || undefined; const status = url.searchParams.get('status') || undefined; const page = parseInt(url.searchParams.get('page') || '1', 10); const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10); - + // console.log('加载提示词模板参数:', { name, type, status, page, pageSize }); - + // 从 API 获取数据 const result = await getPromptTemplates({ name, @@ -63,7 +60,7 @@ export async function loader({ request }: LoaderFunctionArgs) { status, page, pageSize - }); + }, frontendJWT); if (result.error) { console.error('获取提示词模板失败:', result.error); @@ -102,12 +99,43 @@ export async function loader({ request }: LoaderFunctionArgs) { } } +// Action函数 - 处理删除请求 +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + const id = formData.get("id") as string; + const intent = formData.get("intent") as string; + + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + + if (intent === "delete" && id) { + try { + const result = await deletePromptTemplate(id, frontendJWT); + + if (result.error) { + return Response.json({ success: false, error: result.error }, { status: result.status || 500 }); + } + + return Response.json({ success: true }); + } catch (error) { + return Response.json( + { success: false, error: error instanceof Error ? error.message : "删除提示词模板失败" }, + { status: 500 } + ); + } + } + + return Response.json({ success: false, error: "无效的操作" }, { status: 400 }); +} + // 页面组件 export default function PromptsIndex() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const { templates, total, currentPage, pageSize, error } = useLoaderData(); const [isLoading, setIsLoading] = useState(false); + const fetcher = useFetcher(); // 处理搜索名称 const handleNameSearch = (value: string) => { @@ -176,26 +204,28 @@ export default function PromptsIndex() { }; // 删除模板 - const handleDeleteTemplate = async (id: string) => { + const handleDeleteTemplate = (id: string) => { if (confirm('确定要删除该模板吗?删除后无法恢复。')) { - setIsLoading(true); - try { - const result = await deletePromptTemplate(id); - if (result.error) { - alert(`删除失败: ${result.error}`); - } else { - alert('删除成功!'); - // 刷新页面 - window.location.reload(); - } - } catch (error) { - alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`); - } finally { - setIsLoading(false); - } + const formData = new FormData(); + formData.append('id', id); + formData.append('intent', 'delete'); + + fetcher.submit(formData, { method: 'post' }); } }; + // 监听 fetcher 状态变化 + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.data) { + if (fetcher.data.success) { + alert('删除成功!'); + window.location.reload(); + } else if (fetcher.data.error) { + alert(`删除失败: ${fetcher.data.error}`); + } + } + }, [fetcher.state, fetcher.data]); + // 处理分页 const handlePageChange = (page: number) => { const newParams = new URLSearchParams(searchParams); diff --git a/app/routes/prompts.new.tsx b/app/routes/prompts.new.tsx index fa7da43..3fb158d 100644 --- a/app/routes/prompts.new.tsx +++ b/app/routes/prompts.new.tsx @@ -64,28 +64,32 @@ interface ActionData { // 加载函数 export async function loader({ request }: LoaderFunctionArgs) { try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const url = new URL(request.url); const id = url.searchParams.get("id"); const mode = url.searchParams.get("mode") || "create"; - + // 模板数据,如果是新建则为空 let template = null; - + if (id) { // 从API获取数据 - const result = await getPromptTemplate(id); - + const result = await getPromptTemplate(id, frontendJWT); + if (result.error) { console.error('获取提示词模板失败:', result.error); throw new Error(result.error); } - + template = result.data || null; if (!template) { throw new Error(`未找到ID为${id}的模板`); } } - + return Response.json({ template, mode @@ -104,6 +108,9 @@ export async function loader({ request }: LoaderFunctionArgs) { // Action函数 - 处理表单提交 export async function action({ request }: ActionFunctionArgs) { + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const formData = await request.formData(); const id = formData.get("id") as string; const template_name = formData.get("template_name") as string; @@ -158,10 +165,10 @@ export async function action({ request }: ActionFunctionArgs) { let result; if (id) { // 更新模板 - result = await updatePromptTemplate(id, apiTemplate); + result = await updatePromptTemplate(id, apiTemplate, frontendJWT); } else { // 创建模板 - result = await createPromptTemplate(apiTemplate); + result = await createPromptTemplate(apiTemplate, frontendJWT); } if (result.error) { diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 6f1a23b..107fdc7 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -238,13 +238,13 @@ export async function action({ request }: ActionFunctionArgs) { try { const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, result, message, request); - + if (response.error) { console.error('updateReviewResult返回错误:', response.error); return Response.json({ success: false, error: response.error }, { status: response.status || 500 }); } - - return Response.json({ success: true, data: response.data, intent: "confirmReviewResults" }); + + return Response.json({ success: true, data: response.data, intent: "updateReviewResult" }); } catch (updateError) { console.error('调用updateReviewResult时发生异常:', updateError); return Response.json({ diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 52ce0ed..6b1f481 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -21,13 +21,17 @@ export const meta: MetaFunction = () => { ]; }; -export async function loader() { +export async function loader({ request }: { request: Request }) { try { - const response = await getRuleGroups(); + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + + const response = await getRuleGroups(frontendJWT); if (response.error) { throw new Error(response.error); } - return Response.json({ groups: response.data }); + return Response.json({ groups: response.data, frontendJWT }); } catch (error) { console.error('加载评查点分组失败:', error); return Response.json({ groups: [] }); @@ -35,7 +39,7 @@ export async function loader() { } export default function RuleGroupsIndex() { - const { groups: initialGroups } = useLoaderData(); + const { groups: initialGroups, frontendJWT } = useLoaderData(); const rootData = useRouteLoaderData("root") as { userRole: string }; const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); @@ -65,7 +69,7 @@ export default function RuleGroupsIndex() { // 并行加载所有父分组的子分组 const promises = initialGroups.map(async (group: RuleGroup) => { try { - const response = await getChildGroups(group.id); + const response = await getChildGroups(group.id, frontendJWT); if (response.error) { console.error(`加载分组 ${group.id} 的子分组失败:`, response.error); return { parentId: group.id, children: [] }; @@ -139,7 +143,7 @@ export default function RuleGroupsIndex() { } // 否则加载子分组 - const response = await getChildGroups(groupId); + const response = await getChildGroups(groupId, frontendJWT); if (response.error) { throw new Error(response.error); } @@ -187,7 +191,7 @@ export default function RuleGroupsIndex() { const handleDeleteGroup = async (groupId: string) => { if (confirm("确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。")) { try { - const result = await deleteRuleGroup(groupId); + const result = await deleteRuleGroup(groupId, frontendJWT); if (result.success) { // 从本地状态中移除被删除的分组 setGroups(prev => { diff --git a/app/routes/rule-groups.new.tsx b/app/routes/rule-groups.new.tsx index 6d57946..417dcdc 100644 --- a/app/routes/rule-groups.new.tsx +++ b/app/routes/rule-groups.new.tsx @@ -91,12 +91,16 @@ function mapApiToFrontend(apiGroup: ApiRuleGroup): RuleGroup { export async function loader({ request }: LoaderFunctionArgs) { // console.log("rule-groups.new loader被调用,URL:", request.url); try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + const url = new URL(request.url); const id = url.searchParams.get("id"); // console.log("获取到的ID参数:", id); // 获取一级分组列表 (用于选择父级分组) - const parentGroupsResponse = await getRuleGroups(); + const parentGroupsResponse = await getRuleGroups(frontendJWT); if (parentGroupsResponse.error) { console.error("获取父分组列表失败:", parentGroupsResponse.error); throw new Error(parentGroupsResponse.error); @@ -112,7 +116,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 如果有ID,获取分组详情 if (id) { - const groupResponse = await getRuleGroup(id); + const groupResponse = await getRuleGroup(id, frontendJWT); if (groupResponse.error) { console.error("获取分组详情失败:", groupResponse.error); throw new Error(groupResponse.error); @@ -146,6 +150,10 @@ export async function loader({ request }: LoaderFunctionArgs) { export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + // 提取表单数据 const id = formData.get("id") as string | null; const name = formData.get("name") as string; @@ -193,9 +201,9 @@ export async function action({ request }: ActionFunctionArgs) { // 根据是否有ID决定是创建还是更新 let response; if (id) { - response = await updateRuleGroup(id, saveData); + response = await updateRuleGroup(id, saveData, frontendJWT); } else { - response = await createRuleGroup(saveData); + response = await createRuleGroup(saveData, frontendJWT); } // 处理API响应 diff --git a/app/routes/rules-files.tsx b/app/routes/rules-files.tsx index 981daa8..92ab547 100644 --- a/app/routes/rules-files.tsx +++ b/app/routes/rules-files.tsx @@ -71,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) { try { // 获取文档类型列表 - const typesResponse = await getDocumentTypes({pageSize:500}); + const typesResponse = await getDocumentTypes({pageSize:500}, frontendJWT); const documentTypes = typesResponse.data?.types || []; // 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据 @@ -175,7 +175,7 @@ export default function RulesFiles() { const userId = userInfo?.user_id?.toString(); // 获取文件列表 - const filesResponse = await getReviewFiles(searchParams, null, userId); + const filesResponse = await getReviewFiles({...searchParams, token: frontendJWT}, null, userId); if (filesResponse.error) { throw new Error(filesResponse.error); } @@ -240,14 +240,17 @@ export default function RulesFiles() { // 从loader data中获取用户ID const userId = userInfo?.user_id?.toString(); - + + // 添加 token 参数到 apiSearchParams + apiSearchParams.token = frontendJWT; + // 获取文件列表 getReviewFiles(apiSearchParams, null, userId) .then(filesResponse => { if (filesResponse.error) { throw new Error(filesResponse.error); } - + setFiles(filesResponse.data?.files || []); setTotalCount(filesResponse.data?.total || 0); }) @@ -335,7 +338,7 @@ export default function RulesFiles() { return; } - const response = await updateDocumentAuditStatus(fileId, 2, userId); + const response = await updateDocumentAuditStatus(fileId, 2, userId, frontendJWT); if (response.error) { throw new Error(response.error); } diff --git a/app/routes/rules-new.tsx b/app/routes/rules-new.tsx index e18d437..e7def2c 100644 --- a/app/routes/rules-new.tsx +++ b/app/routes/rules-new.tsx @@ -153,9 +153,10 @@ export default function RuleNew() { const [isEditMode, setIsEditMode] = useState(false); const [isLoading, setIsLoading] = useState(false); const [instanceKey, setInstanceKey] = useState('new'); - // 从root路由获取用户角色,而不是从sessionStorage - const rootData = useRouteLoaderData("root") as { userRole: UserRole }; + // 从root路由获取用户角色和JWT token + const rootData = useRouteLoaderData("root") as { userRole: UserRole; frontendJWT?: string }; const userRole = rootData?.userRole || 'common'; + const frontendJWT = rootData?.frontendJWT; const [formData, setFormData] = useState({}); const [evaluationPointGroups, setEvaluationPointGroups] = useState([]); @@ -284,7 +285,8 @@ export default function RuleNew() { const postgrestParams = { filter: { 'id': `eq.${id}` - } + }, + token: frontendJWT }; const response = await postgrestGet('evaluation_points', postgrestParams); @@ -332,7 +334,7 @@ export default function RuleNew() { } finally { setIsLoading(false); } - }, [navigate, extractFieldsFromFormData, resetFormData]); + }, [navigate, extractFieldsFromFormData, resetFormData, frontendJWT]); /** * 获取评查点组数据 @@ -341,7 +343,7 @@ export default function RuleNew() { const fetchEvaluationPointGroups = useCallback(async () => { try { // console.log("获取评查点组数据"); - const response = await postgrestGet('evaluation_point_groups'); + const response = await postgrestGet('evaluation_point_groups', { token: frontendJWT }); if (response.data && Array.isArray(response.data) && response.data.length > 0) { setEvaluationPointGroups(response.data); @@ -351,7 +353,7 @@ export default function RuleNew() { // 显示错误提示但不影响应用继续使用 toastService.error(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`); } - }, []); + }, [frontendJWT]); const handleSave = async () => { // console.log("保存评查点", formData); @@ -582,9 +584,9 @@ export default function RuleNew() { let response; if (isEditMode) { - response = await postgrestPut('evaluation_points', finalData, {id: formData.id!}); + response = await postgrestPut('evaluation_points', finalData, {id: formData.id!}, frontendJWT); } else { - response = await postgrestPost('evaluation_points', finalData); + response = await postgrestPost('evaluation_points', finalData, frontendJWT); } if (response.error) { diff --git a/app/routes/rules._index.tsx b/app/routes/rules._index.tsx index 54282c0..b37e65d 100644 --- a/app/routes/rules._index.tsx +++ b/app/routes/rules._index.tsx @@ -97,8 +97,12 @@ export async function loader({ request }: LoaderFunctionArgs) { }; try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + // 获取评查点类型列表,供前端筛选使用 - const typeResponse = await getRuleTypes(); + const typeResponse = await getRuleTypes(undefined, frontendJWT); if (typeResponse.error) { console.error('获取评查点类型失败:', typeResponse.error); @@ -113,7 +117,8 @@ export async function loader({ request }: LoaderFunctionArgs) { currentPage: params.page, pageSize: params.pageSize, ruleTypes, - initialLoad: true + initialLoad: true, + frontendJWT }, { headers: { "Cache-Control": "max-age=60, s-maxage=180" @@ -139,11 +144,15 @@ export async function action({ request }: LoaderFunctionArgs) { } try { + // 获取用户会话信息 + const { getUserSession } = await import("~/api/login/auth.server"); + const { frontendJWT } = await getUserSession(request); + if (_action === 'delete') { // 调用API删除评查点 // console.log(`删除评查点 ${ruleId}`); - const deleteResponse = await deleteRule(ruleId as string); + const deleteResponse = await deleteRule(ruleId as string, frontendJWT); if (deleteResponse.error) { return Response.json({ result: false, message: deleteResponse.error }, { status: deleteResponse.status || 500 }); @@ -257,7 +266,7 @@ export default function RulesIndex() { // 获取评查点类型 try { - const typeResponse = await getRuleTypes(typeToUse); + const typeResponse = await getRuleTypes(typeToUse, loaderData.frontendJWT); if (typeResponse.data) { setRuleTypes(typeResponse.data); } @@ -273,7 +282,8 @@ export default function RulesIndex() { keyword: searchParams.get('keyword') || undefined, page: currentPage, pageSize, - reviewType: typeToUse + reviewType: typeToUse, + token: loaderData.frontendJWT }; // 调用 API 获取数据 @@ -307,7 +317,7 @@ export default function RulesIndex() { const loadRuleGroups = async () => { setLoadingGroups(true); try { - const response = await getRuleGroupsByType(ruleTypeParam); + const response = await getRuleGroupsByType(ruleTypeParam, loaderData.frontendJWT); if (response.data) { setRuleGroups(response.data); } else if (response.error) { diff --git a/app/styles/main.css b/app/styles/main.css index 2d91c53..3c8c743 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -138,6 +138,27 @@ shadow-[0_0_15px_rgba(0,0,0,0.05)]; } + /* 侧边栏滚动区域样式 */ + .sidebar-scroll-area { + } + + /* 滚动条样式 - 与背景色一致 */ + .sidebar-scroll-area::-webkit-scrollbar { + width: 8px; + } + + .sidebar-scroll-area::-webkit-scrollbar-track { + @apply bg-white; + } + + .sidebar-scroll-area::-webkit-scrollbar-thumb { + @apply bg-gray-400 rounded; + } + + .sidebar-scroll-area::-webkit-scrollbar-thumb:hover { + @apply bg-gray-500; + } + .sidebar.collapsed { @apply w-20; } diff --git a/app/utils/jwt.ts b/app/utils/jwt.ts index 535eca7..2f32929 100644 --- a/app/utils/jwt.ts +++ b/app/utils/jwt.ts @@ -11,8 +11,14 @@ import jwt from 'jsonwebtoken'; const { sign, verify, decode } = jwt; -// JWT密钥 - 在生产环境中应该从环境变量读取 -const JWT_SECRET = 'gdyc-super-secrets-jjwtt-key-change-this-in-production-20250721-from-login-callback'; +// JWT密钥 - 从环境变量读取,如果未设置则抛出错误 +const JWT_SECRET: string = (() => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error('JWT_SECRET environment variable is not set. Please add it to your .env file.'); + } + return secret; +})(); // JWT配置 const JWT_CONFIG = { @@ -104,13 +110,19 @@ export class JWTUtils { */ static verifyJWT(token: string): { valid: boolean; payload?: JWTPayload; error?: string } { try { - const payload = verify(token, JWT_SECRET, { + const decoded = verify(token, JWT_SECRET, { algorithms: [JWT_CONFIG.algorithm], issuer: JWT_CONFIG.issuer, audience: JWT_CONFIG.audience - }) as JWTPayload; + }); - return { valid: true, payload }; + // 验证返回的payload是否包含必需字段 + if (typeof decoded === 'object' && decoded !== null && 'sub' in decoded) { + const payload = decoded as JWTPayload; + return { valid: true, payload }; + } + + return { valid: false, error: 'JWT载荷格式不正确' }; } catch (error) { if (error instanceof Error) { return { valid: false, error: error.message }; diff --git a/deploy.sh b/deploy.sh index dcd7677..c179a66 100755 --- a/deploy.sh +++ b/deploy.sh @@ -22,9 +22,24 @@ if [ ! -f .env ]; then echo "NEXT_PUBLIC_API_URL=" echo "NEXT_PUBLIC_APP_ID=" echo "NEXT_PUBLIC_APP_KEY=" + echo "JWT_SECRET=" exit 1 fi +# 检查 JWT_SECRET 是否配置 +if ! grep -q "^JWT_SECRET=" .env; then + echo "⚠️ 警告: .env 文件中未配置 JWT_SECRET!" + echo "JWT_SECRET 用于签名和验证 JWT token,是必需的安全配置。" + echo "请在 .env 文件中添加:" + echo "JWT_SECRET=your-strong-random-secret-key" + echo "" + echo "可以使用以下命令生成强随机密钥:" + echo "npm run generate:jwt-secret" + exit 1 +fi + +echo "✅ 环境变量检查通过" + # 创建日志目录 mkdir -p logs diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index e0eadaf..a998d03 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -28,7 +28,8 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'production', NEXT_PUBLIC_PORT: '51703', NEXT_PUBLIC_CLIENT_ID: 'meizhou', - NEXT_PUBLIC_API_PORT_CONFIG: '51703' + NEXT_PUBLIC_API_PORT_CONFIG: '51703', + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/meizhou-err.log', out_file: './logs/meizhou-out.log', @@ -60,7 +61,8 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'production', NEXT_PUBLIC_PORT: '51704', NEXT_PUBLIC_CLIENT_ID: 'yunfu', - NEXT_PUBLIC_API_PORT_CONFIG: '51704' + NEXT_PUBLIC_API_PORT_CONFIG: '51704', + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/yunfu-err.log', out_file: './logs/yunfu-out.log', @@ -91,7 +93,8 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'production', NEXT_PUBLIC_PORT: '51705', NEXT_PUBLIC_CLIENT_ID: 'jieyang', - NEXT_PUBLIC_API_PORT_CONFIG: '51705' + NEXT_PUBLIC_API_PORT_CONFIG: '51705', + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/jieyang-err.log', out_file: './logs/jieyang-out.log', @@ -122,7 +125,8 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'production', NEXT_PUBLIC_PORT: '51706', NEXT_PUBLIC_CLIENT_ID: 'chaozhou', - NEXT_PUBLIC_API_PORT_CONFIG: '51706' + NEXT_PUBLIC_API_PORT_CONFIG: '51706', + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/chaozhou-err.log', out_file: './logs/chaozhou-out.log', @@ -153,7 +157,8 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'production', NEXT_PUBLIC_PORT: '51707', NEXT_PUBLIC_CLIENT_ID: 'province', - NEXT_PUBLIC_API_PORT_CONFIG: '51707' + NEXT_PUBLIC_API_PORT_CONFIG: '51707', + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/province-err.log', out_file: './logs/province-out.log', @@ -184,7 +189,8 @@ module.exports = { // NEXT_PUBLIC_NODE_ENV: 'production', // NEXT_PUBLIC_PORT: '51708', // NEXT_PUBLIC_CLIENT_ID: 'province', - // NEXT_PUBLIC_API_PORT_CONFIG: '51708' + // NEXT_PUBLIC_API_PORT_CONFIG: '51708', + // OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' // }, // error_file: './logs/province-err.log', // out_file: './logs/province-out.log', diff --git a/ecosystemDev.config.cjs b/ecosystemDev.config.cjs index dbd125c..2d8154f 100644 --- a/ecosystemDev.config.cjs +++ b/ecosystemDev.config.cjs @@ -28,7 +28,7 @@ module.exports = { NEXT_PUBLIC_PORT: '51703', NEXT_PUBLIC_CLIENT_ID: 'main', NEXT_PUBLIC_API_PORT_CONFIG: '51703', - // REMIX_DEV_ORIGIN: 'http://localhost:51703' + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, env_testing: { NODE_ENV: 'testing', @@ -39,7 +39,8 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_PORT: '51703', NEXT_PUBLIC_CLIENT_ID: 'main', - NEXT_PUBLIC_API_PORT_CONFIG: '51703' + NEXT_PUBLIC_API_PORT_CONFIG: '51703', + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/main-err.log', out_file: './logs/main-out.log', @@ -72,7 +73,8 @@ module.exports = { NEXT_PUBLIC_PORT: '51704', NEXT_PUBLIC_CLIENT_ID: 'chaozhou', NEXT_PUBLIC_API_PORT_CONFIG: '51704', - // REMIX_DEV_ORIGIN: 'http://localhost:51704' + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, env_testing: { NODE_ENV: 'testing', @@ -83,7 +85,9 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_PORT: '51704', NEXT_PUBLIC_CLIENT_ID: 'chaozhou', - NEXT_PUBLIC_API_PORT_CONFIG: '51704' + NEXT_PUBLIC_API_PORT_CONFIG: '51704', + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/chaozhou-err.log', out_file: './logs/chaozhou-out.log', @@ -115,7 +119,8 @@ module.exports = { NEXT_PUBLIC_PORT: '51705', NEXT_PUBLIC_CLIENT_ID: 'jieyang', NEXT_PUBLIC_API_PORT_CONFIG: '51705', - // REMIX_DEV_ORIGIN: 'http://localhost:51705' + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, env_testing: { NODE_ENV: 'testing', @@ -126,7 +131,9 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_PORT: '51705', NEXT_PUBLIC_CLIENT_ID: 'jieyang', - NEXT_PUBLIC_API_PORT_CONFIG: '51705' + NEXT_PUBLIC_API_PORT_CONFIG: '51705', + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/jieyang-err.log', out_file: './logs/jieyang-out.log', @@ -158,7 +165,8 @@ module.exports = { NEXT_PUBLIC_PORT: '51706', NEXT_PUBLIC_CLIENT_ID: 'yunfu', NEXT_PUBLIC_API_PORT_CONFIG: '51706', - // REMIX_DEV_ORIGIN: 'http://localhost:51706' + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, env_testing: { NODE_ENV: 'testing', @@ -169,7 +177,9 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_PORT: '51706', NEXT_PUBLIC_CLIENT_ID: 'yunfu', - NEXT_PUBLIC_API_PORT_CONFIG: '51706' + NEXT_PUBLIC_API_PORT_CONFIG: '51706', + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/yunfu-err.log', out_file: './logs/yunfu-out.log', @@ -201,7 +211,8 @@ module.exports = { NEXT_PUBLIC_PORT: '51707', NEXT_PUBLIC_CLIENT_ID: 'meizhou', NEXT_PUBLIC_API_PORT_CONFIG: '51707', - // REMIX_DEV_ORIGIN: 'http://localhost:51707' + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, env_testing: { NODE_ENV: 'testing', @@ -212,7 +223,9 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_PORT: '51707', NEXT_PUBLIC_CLIENT_ID: 'meizhou', - NEXT_PUBLIC_API_PORT_CONFIG: '51707' + NEXT_PUBLIC_API_PORT_CONFIG: '51707', + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/meizhou-err.log', out_file: './logs/meizhou-out.log', @@ -244,7 +257,8 @@ module.exports = { NEXT_PUBLIC_PORT: '51708', NEXT_PUBLIC_CLIENT_ID: 'province', NEXT_PUBLIC_API_PORT_CONFIG: '51708', - // REMIX_DEV_ORIGIN: 'http://localhost:51708' + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, env_testing: { NODE_ENV: 'testing', @@ -255,7 +269,9 @@ module.exports = { NEXT_PUBLIC_NODE_ENV: 'testing', NEXT_PUBLIC_PORT: '51708', NEXT_PUBLIC_CLIENT_ID: 'province', - NEXT_PUBLIC_API_PORT_CONFIG: '51708' + NEXT_PUBLIC_API_PORT_CONFIG: '51708', + // 🔒 OAuth 敏感配置 + OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' }, error_file: './logs/province-err.log', out_file: './logs/province-out.log', diff --git a/package.json b/package.json index 4800d6d..cb3a310 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "start": "node -r dotenv/config ./node_modules/.bin/remix-serve ./build/server/index.js", "start:pm2:multi": "npm run build:test:multi && pm2 start ecosystemDev.config.cjs --env testing", "start:pm2:production:multi": "npm run build:production:multi && pm2 start ecosystem.config.cjs --env production", - "typecheck": "tsc" + "typecheck": "tsc", + "generate:jwt-secret": "node scripts/generate-jwt-secret.js" }, "dependencies": { "@ant-design/icons": "^5.6.1", diff --git a/public/智慧法务平台操作手册.pdf b/public/智慧法务平台操作手册.pdf new file mode 100644 index 0000000..4d6de4a Binary files /dev/null and b/public/智慧法务平台操作手册.pdf differ diff --git a/scripts/find-postgrest-calls.cjs b/scripts/find-postgrest-calls.cjs new file mode 100644 index 0000000..cc504c6 --- /dev/null +++ b/scripts/find-postgrest-calls.cjs @@ -0,0 +1,186 @@ +/** + * 查找所有使用 postgrest 函数的文件 + * 使用方法: node scripts/find-postgrest-calls.js + */ + +const fs = require('fs'); +const path = require('path'); + +const postgrestFunctions = [ + 'postgrestGet', + 'postgrestPost', + 'postgrestPut', + 'postgrestDelete' +]; + +function findPostgrestCalls(dir, results = {}) { + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + // 跳过 node_modules 和其他不相关目录 + if (stat.isDirectory()) { + if (!file.startsWith('.') && file !== 'node_modules' && file !== 'build') { + findPostgrestCalls(filePath, results); + } + continue; + } + + // 只检查 .ts 和 .tsx 文件 + if (!file.endsWith('.ts') && !file.endsWith('.tsx')) { + continue; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); + + // 查找 postgrest 函数调用 + const calls = []; + lines.forEach((line, index) => { + postgrestFunctions.forEach(fn => { + if (line.includes(`${fn}(`)) { + calls.push({ + line: index + 1, + code: line.trim(), + function: fn + }); + } + }); + }); + + if (calls.length > 0) { + const relativePath = path.relative(process.cwd(), filePath); + results[relativePath] = calls; + } + } + + return results; +} + +// 执行查找 +console.log('🔍 正在查找所有 postgrest 调用...\n'); +const appDir = path.join(process.cwd(), 'app'); +const results = findPostgrestCalls(appDir); + +// 分类显示结果 +const apiFiles = []; +const routeFiles = []; +const otherFiles = []; + +Object.keys(results).forEach(file => { + if (file.includes('app/api/')) { + apiFiles.push(file); + } else if (file.includes('app/routes/')) { + routeFiles.push(file); + } else { + otherFiles.push(file); + } +}); + +// 显示统计 +console.log('📊 统计结果'); +console.log('====================================='); +console.log(`API 文件: ${apiFiles.length} 个`); +console.log(`路由文件: ${routeFiles.length} 个`); +console.log(`其他文件: ${otherFiles.length} 个`); +console.log(`总计: ${Object.keys(results).length} 个文件\n`); + +// 显示 API 文件 +if (apiFiles.length > 0) { + console.log('📁 API 文件'); + console.log('====================================='); + apiFiles.forEach(file => { + console.log(`\n${file}`); + results[file].forEach(call => { + console.log(` 行 ${call.line}: ${call.function}()`); + }); + }); +} + +// 显示路由文件 +if (routeFiles.length > 0) { + console.log('\n\n📄 路由文件'); + console.log('====================================='); + routeFiles.forEach(file => { + console.log(`\n${file}`); + results[file].forEach(call => { + console.log(` 行 ${call.line}: ${call.function}()`); + }); + }); +} + +// 显示其他文件 +if (otherFiles.length > 0) { + console.log('\n\n📦 其他文件'); + console.log('====================================='); + otherFiles.forEach(file => { + console.log(`\n${file}`); + results[file].forEach(call => { + console.log(` 行 ${call.line}: ${call.function}()`); + }); + }); +} + +// 生成 Markdown 清单 +const mdContent = `# PostgreSQL 调用清单 + +生成时间: ${new Date().toLocaleString('zh-CN')} + +## 统计 + +- API 文件: ${apiFiles.length} 个 +- 路由文件: ${routeFiles.length} 个 +- 其他文件: ${otherFiles.length} 个 +- **总计: ${Object.keys(results).length} 个文件** + +## API 文件 + +${apiFiles.map(file => `- [ ] \`${file}\` (${results[file].length} 处调用)`).join('\n')} + +## 路由文件 + +${routeFiles.map(file => `- [ ] \`${file}\` (${results[file].length} 处调用)`).join('\n')} + +${otherFiles.length > 0 ? `## 其他文件\n\n${otherFiles.map(file => `- [ ] \`${file}\` (${results[file].length} 处调用)`).join('\n')}` : ''} + +## 修改建议 + +### 优先级 1:路由文件(高优先级) +路由文件直接处理用户请求,应优先修改: + +${routeFiles.slice(0, 5).map((file, i) => `${i + 1}. \`${file}\``).join('\n')} + +### 优先级 2:API 文件(中优先级) +API 文件被路由调用,可以创建带 JWT 的封装: + +${apiFiles.slice(0, 5).map((file, i) => `${i + 1}. \`${file}\``).join('\n')} + +### 修改模板 + +\`\`\`typescript +// 在 loader/action 中 +import { getJwtFromRequest } from "~/api/jwt-helper.server"; + +export async function loader({ request }: LoaderFunctionArgs) { + const jwt = await getJwtFromRequest(request); + + // 方式 1: 直接传递 + const response = await postgrestGet('table_name', { token: jwt }); + + // 方式 2: 使用现有参数 + const response = await postgrestGet('table_name', { + ...otherParams, + token: jwt + }); + + return json(response); +} +\`\`\` +`; + +fs.writeFileSync('POSTGREST_CALLS_CHECKLIST.md', mdContent); +console.log('\n\n✅ 已生成清单文件: POSTGREST_CALLS_CHECKLIST.md'); +console.log('\n💡 提示: 可以用这个文件追踪修改进度'); + diff --git a/scripts/find-postgrest-calls.js b/scripts/find-postgrest-calls.js new file mode 100644 index 0000000..cc504c6 --- /dev/null +++ b/scripts/find-postgrest-calls.js @@ -0,0 +1,186 @@ +/** + * 查找所有使用 postgrest 函数的文件 + * 使用方法: node scripts/find-postgrest-calls.js + */ + +const fs = require('fs'); +const path = require('path'); + +const postgrestFunctions = [ + 'postgrestGet', + 'postgrestPost', + 'postgrestPut', + 'postgrestDelete' +]; + +function findPostgrestCalls(dir, results = {}) { + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + // 跳过 node_modules 和其他不相关目录 + if (stat.isDirectory()) { + if (!file.startsWith('.') && file !== 'node_modules' && file !== 'build') { + findPostgrestCalls(filePath, results); + } + continue; + } + + // 只检查 .ts 和 .tsx 文件 + if (!file.endsWith('.ts') && !file.endsWith('.tsx')) { + continue; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); + + // 查找 postgrest 函数调用 + const calls = []; + lines.forEach((line, index) => { + postgrestFunctions.forEach(fn => { + if (line.includes(`${fn}(`)) { + calls.push({ + line: index + 1, + code: line.trim(), + function: fn + }); + } + }); + }); + + if (calls.length > 0) { + const relativePath = path.relative(process.cwd(), filePath); + results[relativePath] = calls; + } + } + + return results; +} + +// 执行查找 +console.log('🔍 正在查找所有 postgrest 调用...\n'); +const appDir = path.join(process.cwd(), 'app'); +const results = findPostgrestCalls(appDir); + +// 分类显示结果 +const apiFiles = []; +const routeFiles = []; +const otherFiles = []; + +Object.keys(results).forEach(file => { + if (file.includes('app/api/')) { + apiFiles.push(file); + } else if (file.includes('app/routes/')) { + routeFiles.push(file); + } else { + otherFiles.push(file); + } +}); + +// 显示统计 +console.log('📊 统计结果'); +console.log('====================================='); +console.log(`API 文件: ${apiFiles.length} 个`); +console.log(`路由文件: ${routeFiles.length} 个`); +console.log(`其他文件: ${otherFiles.length} 个`); +console.log(`总计: ${Object.keys(results).length} 个文件\n`); + +// 显示 API 文件 +if (apiFiles.length > 0) { + console.log('📁 API 文件'); + console.log('====================================='); + apiFiles.forEach(file => { + console.log(`\n${file}`); + results[file].forEach(call => { + console.log(` 行 ${call.line}: ${call.function}()`); + }); + }); +} + +// 显示路由文件 +if (routeFiles.length > 0) { + console.log('\n\n📄 路由文件'); + console.log('====================================='); + routeFiles.forEach(file => { + console.log(`\n${file}`); + results[file].forEach(call => { + console.log(` 行 ${call.line}: ${call.function}()`); + }); + }); +} + +// 显示其他文件 +if (otherFiles.length > 0) { + console.log('\n\n📦 其他文件'); + console.log('====================================='); + otherFiles.forEach(file => { + console.log(`\n${file}`); + results[file].forEach(call => { + console.log(` 行 ${call.line}: ${call.function}()`); + }); + }); +} + +// 生成 Markdown 清单 +const mdContent = `# PostgreSQL 调用清单 + +生成时间: ${new Date().toLocaleString('zh-CN')} + +## 统计 + +- API 文件: ${apiFiles.length} 个 +- 路由文件: ${routeFiles.length} 个 +- 其他文件: ${otherFiles.length} 个 +- **总计: ${Object.keys(results).length} 个文件** + +## API 文件 + +${apiFiles.map(file => `- [ ] \`${file}\` (${results[file].length} 处调用)`).join('\n')} + +## 路由文件 + +${routeFiles.map(file => `- [ ] \`${file}\` (${results[file].length} 处调用)`).join('\n')} + +${otherFiles.length > 0 ? `## 其他文件\n\n${otherFiles.map(file => `- [ ] \`${file}\` (${results[file].length} 处调用)`).join('\n')}` : ''} + +## 修改建议 + +### 优先级 1:路由文件(高优先级) +路由文件直接处理用户请求,应优先修改: + +${routeFiles.slice(0, 5).map((file, i) => `${i + 1}. \`${file}\``).join('\n')} + +### 优先级 2:API 文件(中优先级) +API 文件被路由调用,可以创建带 JWT 的封装: + +${apiFiles.slice(0, 5).map((file, i) => `${i + 1}. \`${file}\``).join('\n')} + +### 修改模板 + +\`\`\`typescript +// 在 loader/action 中 +import { getJwtFromRequest } from "~/api/jwt-helper.server"; + +export async function loader({ request }: LoaderFunctionArgs) { + const jwt = await getJwtFromRequest(request); + + // 方式 1: 直接传递 + const response = await postgrestGet('table_name', { token: jwt }); + + // 方式 2: 使用现有参数 + const response = await postgrestGet('table_name', { + ...otherParams, + token: jwt + }); + + return json(response); +} +\`\`\` +`; + +fs.writeFileSync('POSTGREST_CALLS_CHECKLIST.md', mdContent); +console.log('\n\n✅ 已生成清单文件: POSTGREST_CALLS_CHECKLIST.md'); +console.log('\n💡 提示: 可以用这个文件追踪修改进度'); + diff --git a/scripts/generate-jwt-secret.js b/scripts/generate-jwt-secret.js new file mode 100644 index 0000000..56d3fcc --- /dev/null +++ b/scripts/generate-jwt-secret.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * JWT Secret 生成工具 + * + * 用法: + * node scripts/generate-jwt-secret.js + * + * 生成一个强随机的 JWT Secret,可以直接用于 .env 文件 + */ + +const crypto = require('crypto'); + +// 生成 64 字节(128 个十六进制字符)的强随机密钥 +const secret = crypto.randomBytes(64).toString('hex'); + +console.log('\n==================== JWT Secret 生成成功 ====================\n'); +console.log('请将以下内容添加到你的 .env 文件中:\n'); +console.log(`JWT_SECRET=${secret}\n`); +console.log('⚠️ 重要提醒:'); +console.log('1. 不要将此密钥提交到版本控制系统(Git)'); +console.log('2. 生产环境请使用密钥管理服务(如 AWS Secrets Manager)'); +console.log('3. 不同环境(开发/测试/生产)应使用不同的密钥'); +console.log('4. 建议每 3-6 个月轮换一次密钥'); +console.log('5. 密钥长度: 128 个字符(64 字节)\n'); +console.log('============================================================\n'); diff --git a/开发工具和中间件清单.md b/开发工具和中间件清单.md new file mode 100644 index 0000000..509a68f --- /dev/null +++ b/开发工具和中间件清单.md @@ -0,0 +1,386 @@ +# 应用系统开发工具和中间件清单 + +## 开发工具软件 + +### 1. 主要开发工具 + +#### Visual Studio Code (VSCode) +- **产品名称**: Visual Studio Code +- **发行厂商**: Microsoft Corporation +- **版本号**: 最新版本(具体版本号需要查看本地安装) +- **适用范围**: 主要代码编辑器,用于 TypeScript/JavaScript 开发 +- **安全特点**: + - 支持代码签名验证 + - 内置安全扫描功能 + - 支持 Git 集成进行版本控制 + - 扩展市场安全验证 + - 自动更新机制 + +### 2. 编程语言和运行时 + +#### Node.js +- **产品名称**: Node.js +- **发行厂商**: OpenJS Foundation +- **版本号**: `>=20.0.0` (package.json 中指定) +- **适用范围**: JavaScript 运行时环境,用于服务端开发 +- **安全特点**: + - 定期安全更新 + - 内置安全模块 + - 支持 TLS/SSL 加密 + - 内存泄漏防护 + - 进程隔离 + +#### TypeScript +- **产品名称**: TypeScript +- **发行厂商**: Microsoft Corporation +- **版本号**: `^5.1.6` +- **适用范围**: 类型安全的 JavaScript 超集,用于前端和后端开发 +- **安全特点**: + - 编译时类型检查 + - 减少运行时错误 + - 支持严格模式配置 + - 代码重构安全 + - 接口定义安全 + +## 前端开发工具 + +### 3. 前端框架 + +#### React +- **产品名称**: React +- **发行厂商**: Meta (Facebook) +- **版本号**: `^18.2.0` +- **适用范围**: 用户界面构建库 +- **安全特点**: + - XSS 防护 + - 虚拟 DOM 安全渲染 + - 支持内容安全策略 (CSP) + - 组件隔离 + - 安全的 props 传递 + +#### Remix +- **产品名称**: Remix +- **发行厂商**: Remix Software Inc. +- **版本号**: `^2.16.2` +- **适用范围**: 全栈 Web 框架 +- **安全特点**: + - 内置 CSRF 保护 + - 自动 XSS 防护 + - 安全的表单处理 + - 服务端渲染安全 + - 路由安全 + +### 4. 构建工具 + +#### Vite +- **产品名称**: Vite +- **发行厂商**: Evan You (Vue.js 作者) +- **版本号**: `^6.0.0` +- **适用范围**: 前端构建工具和开发服务器 +- **安全特点**: + - 热模块替换安全 + - 支持 HTTPS 开发服务器 + - 依赖预构建安全 + - 源码映射安全 + - 环境变量安全处理 + +#### esbuild +- **产品名称**: esbuild +- **发行厂商**: Evan Wallace +- **版本号**: `^0.25.1` +- **适用范围**: JavaScript 打包工具 +- **安全特点**: + - 快速的代码转换 + - 支持源码映射 + - 安全的依赖解析 + - 内存使用优化 + - 并行处理安全 + +### 5. UI 组件库 + +#### Ant Design +- **产品名称**: Ant Design +- **发行厂商**: Ant Financial (蚂蚁金服) +- **版本号**: `^5.25.4` +- **适用范围**: React UI 组件库 +- **安全特点**: + - 无障碍访问支持 + - 安全的表单组件 + - 支持主题定制 + - 国际化安全 + - 组件生命周期安全 + +## 后端开发工具 + +### 6. 数据库相关 + +#### PostgreSQL 客户端 +- **产品名称**: pg (node-postgres) +- **发行厂商**: Brian Carlson +- **版本号**: `^8.14.1` +- **适用范围**: PostgreSQL 数据库客户端驱动 +- **安全特点**: + - 参数化查询防注入 + - SSL 连接支持 + - 连接池管理 + - 事务安全 + - 错误处理安全 + +### 7. 进程管理 + +#### PM2 +- **产品名称**: PM2 +- **发行厂商**: Keymetrics +- **版本号**: `^6.0.8` +- **适用范围**: Node.js 进程管理器 +- **安全特点**: + - 进程隔离 + - 自动重启机制 + - 日志管理 + - 监控安全 + - 集群模式安全 + +## 中间件 + +### 8. Web 服务器 + +#### Nginx +- **产品名称**: Nginx +- **发行厂商**: Nginx Inc. (现为 F5 Networks) +- **版本号**: `alpine` (Docker 镜像) +- **适用范围**: 反向代理和负载均衡 +- **安全特点**: + - 请求限流 + - SSL/TLS 终止 + - 安全头部配置 + - DDoS 防护 + - 访问控制 + - 日志安全 + +### 9. 容器化 + +#### Docker +- **产品名称**: Docker +- **发行厂商**: Docker Inc. +- **版本号**: 最新稳定版 +- **适用范围**: 应用容器化部署 +- **安全特点**: + - 容器隔离 + - 镜像签名验证 + - 安全扫描功能 + - 网络隔离 + - 资源限制 + - 运行时安全 + +### 10. 认证中间件 + +#### JWT (jsonwebtoken) +- **产品名称**: jsonwebtoken +- **发行厂商**: Auth0 +- **版本号**: `^9.0.2` +- **适用范围**: 用户身份认证和授权 +- **安全特点**: + - 数字签名验证 + - 令牌过期机制 + - 支持多种算法 + - 令牌刷新机制 + - 黑名单支持 + +## 开发辅助工具 + +### 11. 代码质量工具 + +#### ESLint +- **产品名称**: ESLint +- **发行厂商**: OpenJS Foundation +- **版本号**: `^8.38.0` +- **适用范围**: JavaScript/TypeScript 代码检查 +- **安全特点**: + - 安全规则检查 + - 代码规范统一 + - 潜在漏洞检测 + - 自定义规则支持 + - 集成开发环境支持 + +#### TypeScript ESLint +- **产品名称**: @typescript-eslint +- **发行厂商**: TypeScript ESLint Team +- **版本号**: `^6.7.4` +- **适用范围**: TypeScript 代码检查 +- **安全特点**: + - 类型安全检查 + - 严格的代码规范 + - 最佳实践检查 + - 类型推断安全 + - 接口安全验证 + +### 12. 样式工具 + +#### Tailwind CSS +- **产品名称**: Tailwind CSS +- **发行厂商**: Tailwind Labs +- **版本号**: `^3.4.17` +- **适用范围**: CSS 框架 +- **安全特点**: + - 安全的类名生成 + - 内容安全策略兼容 + - 无 JavaScript 依赖 + - 响应式设计安全 + - 主题系统安全 + +#### PostCSS +- **产品名称**: PostCSS +- **发行厂商**: PostCSS Team +- **版本号**: `^8.5.3` +- **适用范围**: CSS 后处理器 +- **安全特点**: + - 安全的插件系统 + - 源码映射支持 + - 兼容性处理 + - 性能优化 + - 错误处理安全 + +### 13. 版本控制 + +#### Git +- **产品名称**: Git +- **发行厂商**: Linus Torvalds (Linux Foundation) +- **版本号**: 最新稳定版 +- **适用范围**: 代码版本控制 +- **安全特点**: + - 数字签名支持 + - 加密传输 + - 访问控制 + - 分支保护 + - 提交验证 + +## 第三方软件部件 + +### 14. HTTP 客户端 + +#### Axios +- **产品名称**: Axios +- **发行厂商**: Matt Zabriskie +- **版本号**: `^1.9.0` +- **适用范围**: HTTP 请求库 +- **安全特点**: + - 自动 CSRF 令牌处理 + - 请求/响应拦截 + - 错误处理机制 + - 超时控制 + - 重试机制 + +### 15. 工具库 + +#### dayjs +- **产品名称**: dayjs +- **发行厂商**: iamkun +- **版本号**: `^1.11.13` +- **适用范围**: 日期时间处理 +- **安全特点**: + - 不可变对象 + - 时区安全处理 + - 轻量级设计 + - 格式化安全 + - 解析验证 + +#### uuid +- **产品名称**: uuid +- **发行厂商**: LiosK +- **版本号**: `^11.1.0` +- **适用范围**: 唯一标识符生成 +- **安全特点**: + - 加密安全的随机数生成 + - 支持多种 UUID 版本 + - 防碰撞设计 + - 时间戳安全 + - 命名空间支持 + +### 16. 文档处理 + +#### PDF.js +- **产品名称**: pdfjs-dist +- **发行厂商**: Mozilla +- **版本号**: `^3.11.174` +- **适用范围**: PDF 文档渲染 +- **安全特点**: + - 沙箱执行环境 + - 内存使用限制 + - 安全的内容解析 + - 权限控制 + - 漏洞防护 + +#### docx-preview +- **产品名称**: docx-preview +- **发行厂商**: Volodymyr Klymenko +- **版本号**: `^0.3.5` +- **适用范围**: Word 文档预览 +- **安全特点**: + - 客户端渲染 + - 无服务器依赖 + - 安全的文件解析 + - 格式验证 + - 错误处理 + +### 17. 状态管理 + +#### Immer +- **产品名称**: Immer +- **发行厂商**: Michel Weststrate +- **版本号**: `^10.1.1` +- **适用范围**: 不可变状态管理 +- **安全特点**: + - 不可变数据结构 + - 类型安全 + - 性能优化 + - 调试友好 + - 内存安全 + +### 18. 实用工具 + +#### highlight.js +- **产品名称**: highlight.js +- **发行厂商**: highlight.js contributors +- **版本号**: `^11.11.1` +- **适用范围**: 代码语法高亮 +- **安全特点**: + - XSS 防护 + - 安全的语言检测 + - 自定义主题安全 + - 性能优化 + - 错误处理 + +## 安全特性总结 + +### 整体安全架构 +1. **多层防护**: 从开发工具到部署环境的全面安全防护 +2. **代码安全**: 通过 TypeScript、ESLint 等工具确保代码质量 +3. **运行时安全**: Node.js 和容器化提供隔离环境 +4. **网络安全**: Nginx 提供反向代理和安全头部 +5. **数据安全**: PostgreSQL 和 JWT 确保数据访问安全 + +### 安全最佳实践 +- 所有依赖包都使用固定版本号,避免自动升级引入安全风险 +- 使用环境变量管理敏感配置信息 +- 实施最小权限原则 +- 定期更新依赖包以修复安全漏洞 +- 使用 HTTPS 进行数据传输 +- 实施适当的日志记录和监控 +- 代码审查和静态分析 +- 安全测试和渗透测试 + +### 开发环境安全 +- VSCode 提供安全的开发环境 +- 扩展市场验证确保插件安全 +- 自动更新机制及时修复漏洞 +- 集成安全工具链 +- 代码签名和验证 + +### 部署安全 +- Docker 容器化提供隔离环境 +- Nginx 反向代理和安全配置 +- PM2 进程管理和监控 +- 环境变量管理敏感信息 +- 日志记录和审计 + +这套技术栈提供了从开发到部署的完整安全防护体系,确保应用系统的安全性和可靠性。每个组件都经过精心选择,具备相应的安全特性,共同构建了一个安全、稳定、高效的开发和应用环境。 \ No newline at end of file