feat: 1. 添加axios全局路由拦截进行自动添加请求jwt。 2.重新整理路由表。 3. 文档列表新增版本差异对比。 4.菜单路由可访问列表通过对接接口返回,添加全局路由检测。

5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。    6. 修改交叉评查的部分样式
This commit is contained in:
2025-11-18 11:06:24 +08:00
parent 8a50671c39
commit bfe39e45a9
53 changed files with 9503 additions and 2796 deletions
+172 -76
View File
@@ -1,9 +1,10 @@
import { useEffect, useState } from "react";
import { useActionData, useLoaderData, Form } from "@remix-run/react";
import { useLoaderData, useNavigate, useFetcher } from "@remix-run/react";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { CLIENT_OAUTH_CONFIG } from "~/config/api-config";
import { getUserSession, getSession, simpleRootLogin } from "~/api/login/auth.server";
import { getUserSession, getSession, createUserSession } from "~/api/login/auth.server";
import { loginWithPassword } from "~/api/login/login-client";
import styles from "~/styles/pages/login.css?url";
import { toastService } from "~/components/ui";
@@ -18,31 +19,24 @@ export const meta: MetaFunction = () => {
];
};
// 加载器,获取当前会话状态
// 加载器,获取重定向URL和错误信息
export async function loader({ request }: LoaderFunctionArgs) {
const { isAuthenticated } = await getUserSession(request);
// 如果已登录,重定向到首页
if (isAuthenticated) {
return redirect("/");
}
// ⚠️ 不再检查服务端 session 认证
// 认证检查改为在客户端通过 localStorage 进行
// 获取重定向URL并保存到session
// 获取重定向URL
const url = new URL(request.url);
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,
return Response.json({
redirectTo,
flashError: loginError
}, {
@@ -51,65 +45,141 @@ export async function loader({ request }: LoaderFunctionArgs) {
}
});
}
return Response.json({
isAuthenticated: false,
return Response.json({
redirectTo,
flashError: null
});
}
// 处理表单提交的action函数
// 处理管理员账密登录
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
const username = formData.get("username")?.toString().trim();
const password = formData.get("password")?.toString().trim();
try {
const formData = await request.formData();
const username = formData.get("username") as string;
const password = formData.get("password") as string;
const redirectTo = formData.get("redirectTo") as string || "/";
if (intent === "password_login") {
// 获取重定向目标
const session = await getSession(request);
const redirectTo = session.get("redirectTo") || "/";
// 调用 simpleRootLogin 方法进行登录
const response = await simpleRootLogin(username || "", password || "", redirectTo);
// 检查响应状态
if (response.status === 302) {
// 登录成功,直接返回重定向响应
return response;
} else {
// 登录失败,返回错误信息(不再使用URL参数)
const errorData = await response.json();
// 验证输入
if (!username?.trim()) {
return Response.json({
success: false,
error: errorData.error || "登录失败",
retryCount: errorData.retryCount || 0,
isLocked: errorData.isLocked || false,
remainingAttempts: errorData.remainingAttempts || 5
}, {
status: response.status
});
error: "请输入用户名"
}, { status: 400 });
}
}
return null;
if (!password?.trim()) {
return Response.json({
success: false,
error: "请输入密码"
}, { status: 400 });
}
console.log("📝 [Login Action] 开始处理管理员登录:", { username });
// 调用后端登录接口
const response = await loginWithPassword(username.trim(), password.trim());
if (!response.success || !response.data) {
console.error("❌ [Login Action] 登录失败:", response.error);
return Response.json({
success: false,
error: response.error || "登录失败,请检查用户名和密码"
}, { status: 401 });
}
const { access_token, user_info } = response.data;
// 验证返回数据
if (!access_token) {
console.error("❌ [Login Action] 后端未返回 access_token");
return Response.json({
success: false,
error: "登录失败:未获取到认证令牌"
}, { status: 500 });
}
if (!user_info) {
console.error("❌ [Login Action] 后端未返回 user_info");
return Response.json({
success: false,
error: "登录失败:未获取到用户信息"
}, { status: 500 });
}
console.log("✅ [Login Action] 登录成功,准备创建 session");
console.log("👤 [Login Action] 用户角色:", user_info.user_role); // 应该是 "admin"
// 获取当前 URL 用于构建 callback URL
const url = new URL(request.url);
// 🔑 重要:将 token 和用户信息作为 URL 参数传递给客户端
// 复用 OAuth 登录的 callback 页面逻辑
const callbackUrl = new URL('/callback', url.origin);
callbackUrl.searchParams.set('token', access_token);
callbackUrl.searchParams.set('userInfo', encodeURIComponent(JSON.stringify({
user_id: user_info.user_id,
username: user_info.username,
nick_name: user_info.nick_name,
email: user_info.email,
phone_number: user_info.phone_number,
ou_id: user_info.ou_id,
ou_name: user_info.ou_name,
is_leader: user_info.is_leader,
user_role: user_info.user_role,
sub: user_info.sub
})));
callbackUrl.searchParams.set('redirectTo', redirectTo);
// ✅ 使用统一的 session 创建函数(和 OAuth 登录一样)
return createUserSession({
isAuthenticated: true,
userRole: user_info.user_role,
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token
frontendJWT: access_token, // 保存到 Cookie Session
userInfo: {
user_id: user_info.user_id,
username: user_info.username,
nick_name: user_info.nick_name,
email: user_info.email,
phone_number: user_info.phone_number,
ou_id: user_info.ou_id,
ou_name: user_info.ou_name,
is_leader: user_info.is_leader,
user_role: user_info.user_role,
sub: user_info.sub
}
});
} catch (error) {
console.error("❌ [Login Action] 处理登录时发生异常:", error);
return Response.json({
success: false,
error: error instanceof Error ? error.message : "登录失败,请稍后重试"
}, { status: 500 });
}
}
export default function Login() {
const actionData = useActionData<typeof action>();
const navigate = useNavigate();
const loaderData = useLoaderData<typeof loader>();
const fetcher = useFetcher<{ success: boolean; error?: string }>();
const [isFlipped, setIsFlipped] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
// 从 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;
const [passwordLoginError, setPasswordLoginError] = useState<string | null>(null);
// 从 loaderData 中获取 OAuth 回调的错误信息
const oauthError = loaderData?.flashError;
// 显示的错误信息:密码登录错误优先,其次是 OAuth 错误
const error = passwordLoginError || oauthError;
const isLocked = false; // 可以从后端响应中获取
const retryCount = 0;
const remainingAttempts = 5;
// 监听 fetcher 的状态
const isLoading = fetcher.state === "submitting" || fetcher.state === "loading";
// 处理OAuth2.0登录
const handleOAuthLogin = () => {
@@ -148,29 +218,55 @@ export default function Login() {
// 处理账号密码登录表单提交
const handlePasswordLoginSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 清除之前的错误
setPasswordLoginError(null);
// 检查账户是否被锁定
if (isLocked) {
e.preventDefault();
toastService.error("账户已被锁定,请联系管理员");
return;
}
// 客户端验证
if (!username.trim()) {
e.preventDefault();
toastService.error("请输入用户名");
return;
}
if (!password.trim()) {
e.preventDefault();
toastService.error("请输入密码");
return;
}
// 验证通过,让表单正常提交
console.log("📝 [Login] 提交管理员登录表单");
// ✅ 使用 fetcher 提交表单到服务端 action
const formData = new FormData();
formData.append("username", username.trim());
formData.append("password", password.trim());
formData.append("redirectTo", loaderData?.redirectTo || "/");
fetcher.submit(formData, {
method: "post",
action: "/login"
});
};
// 处理 fetcher 响应
useEffect(() => {
if (fetcher.data) {
if (!fetcher.data.success && fetcher.data.error) {
// 登录失败,显示错误
console.error("❌ [Login] 登录失败:", fetcher.data.error);
setPasswordLoginError(fetcher.data.error);
toastService.error(fetcher.data.error);
}
// 登录成功的情况由 action 中的 redirect 处理,会自动跳转到 callback 页面
}
}, [fetcher.data]);
useEffect(() => {
// 检查OAuth配置是否完整(客户端不需要检查 clientSecret
if (!CLIENT_OAUTH_CONFIG.serverUrl || !CLIENT_OAUTH_CONFIG.clientId) {
@@ -279,9 +375,7 @@ export default function Login() {
</div>
)}
<Form method="post" className="admin-login-form" onSubmit={handlePasswordLoginSubmit}>
<input type="hidden" name="intent" value="password_login" />
<form className="admin-login-form" onSubmit={handlePasswordLoginSubmit}>
<div className="form-group">
<label htmlFor="username" className="form-label"></label>
<input
@@ -292,10 +386,11 @@ export default function Login() {
onChange={(e) => setUsername(e.target.value)}
className="form-input"
placeholder="请输入用户名"
disabled={isLoading}
required
/>
</div>
<div className="form-group">
<label htmlFor="password" className="form-label"></label>
<input
@@ -306,18 +401,19 @@ export default function Login() {
onChange={(e) => setPassword(e.target.value)}
className="form-input"
placeholder="请输入密码"
disabled={isLoading}
required
/>
</div>
<button
<button
type="submit"
className="admin-login-button"
disabled={isLocked}
style={isLocked ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
disabled={isLocked || isLoading}
style={(isLocked || isLoading) ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
>
<i className={isLocked ? "ri-lock-line" : "ri-login-box-line"}></i>
{isLocked ? "账户已锁定" : "登录"}
<i className={isLocked ? "ri-lock-line" : isLoading ? "ri-loader-4-line" : "ri-login-box-line"}></i>
{isLocked ? "账户已锁定" : isLoading ? "登录中..." : "登录"}
</button>
{isLocked && (
@@ -334,7 +430,7 @@ export default function Login() {
<i className="ri-information-line"></i>
</div>
)}
</Form>
</form>
<div className="back-to-oauth">
<button