给所有请求都加上jwt,隐藏生成jwt的secret(放到.env中),隐藏app-secret(放在pm2运行配置文件中,后续直接读取环境配置即可)

This commit is contained in:
2025-10-17 15:28:22 +08:00
parent 9ec6d30573
commit 59706b70d0
70 changed files with 2279 additions and 688 deletions
+2 -2
View File
@@ -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');
}
}, []);
+3 -2
View File
@@ -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服务器的响应
+3 -2
View File
@@ -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) {
+55
View File
@@ -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
});
}
}
+44 -15
View File
@@ -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, "登录回调处理失败,请重新登录");
}
}
+13 -6
View File
@@ -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 });
+14 -7
View File
@@ -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);
}
+16 -11
View File
@@ -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);
+75 -70
View File
@@ -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,
+14 -9
View File
@@ -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);
+61 -54
View File
@@ -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<CategoryWithSearchCount> => {
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<CategoryWithSearchCount> => {
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();
+5 -2
View File
@@ -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<typeof loader>();
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}
/>
</div>
);
+3 -3
View File
@@ -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;
+14 -6
View File
@@ -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<LoaderData>();
const { types, total, error, ruleTypes, frontendJWT } = useLoaderData<LoaderData>();
// 状态管理
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
@@ -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) {
+16 -9
View File
@@ -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) {
+23 -20
View File
@@ -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 || '未知错误'));
+4 -1
View File
@@ -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
}
);
+19 -16
View File
@@ -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 (
<div className="preview-content relative overflow-y-auto max-h-[1000px]">
<Document
file={DOCUMENT_URL + documentData.path}
file={`/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={(error) => {
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');
};
+8 -8
View File
@@ -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);
+7 -6
View File
@@ -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<typeof loader>();
const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo, frontendJWT } = useLoaderData<typeof loader>();
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(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);
// 更新文档数据
+98 -43
View File
@@ -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<typeof action>();
const loaderData = useLoaderData<typeof loader>();
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() {
<h2 className="login-subtitle"></h2>
{error && (
<div className="error-message-container">
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
<div className="error-text">{getErrorMessage(error)}</div>
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
<div className="error-icon">
{isLocked ? (
<i className="ri-lock-line"></i>
) : (
<i className="ri-error-warning-line"></i>
)}
</div>
<div className="error-text">
{error}
{!isLocked && retryCount > 0 && (
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
{remainingAttempts}
</div>
)}
</div>
</div>
)}
@@ -235,9 +260,22 @@ export default function Login() {
<h2 className="login-subtitle"></h2>
{error && (
<div className="error-message-container">
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
<div className="error-text">{getErrorMessage(error)}</div>
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
<div className="error-icon">
{isLocked ? (
<i className="ri-lock-line"></i>
) : (
<i className="ri-error-warning-line"></i>
)}
</div>
<div className="error-text">
{error}
{!isLocked && retryCount > 0 && (
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
{remainingAttempts}
</div>
)}
</div>
</div>
)}
@@ -275,10 +313,27 @@ export default function Login() {
<button
type="submit"
className="admin-login-button"
disabled={isLocked}
style={isLocked ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
>
<i className="ri-login-box-line"></i>
<i className={isLocked ? "ri-lock-line" : "ri-login-box-line"}></i>
{isLocked ? "账户已锁定" : "登录"}
</button>
{isLocked && (
<div style={{
marginTop: '12px',
padding: '8px',
background: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '4px',
fontSize: '0.9em',
textAlign: 'center',
color: '#856404'
}}>
<i className="ri-information-line"></i>
</div>
)}
</Form>
<div className="back-to-oauth">
+65 -35
View File
@@ -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<typeof loader>();
const [isLoading, setIsLoading] = useState(false);
const fetcher = useFetcher<ActionData>();
// 处理搜索名称
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);
+15 -8
View File
@@ -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) {
+3 -3
View File
@@ -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({
+11 -7
View File
@@ -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<typeof loader>();
const { groups: initialGroups, frontendJWT } = useLoaderData<typeof loader>();
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 => {
+12 -4
View File
@@ -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响应
+8 -5
View File
@@ -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);
}
+10 -8
View File
@@ -153,9 +153,10 @@ export default function RuleNew() {
const [isEditMode, setIsEditMode] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [instanceKey, setInstanceKey] = useState<string>('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<EvaluationPoint>({});
const [evaluationPointGroups, setEvaluationPointGroups] = useState<EvaluationPointGroup[]>([]);
@@ -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) {
+16 -6
View File
@@ -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) {