Files
leaudit-platform-frontend/app/routes/login.tsx
T

283 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from "react";
import { useSearchParams, 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";
import { getUserSession, getSession, sessionStorage, getUserBySub, addDefaultRole } from "~/api/login/auth.server";
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
import styles from "~/styles/pages/login.css?url";
export const links = () => [
{ rel: "stylesheet", href: styles }
];
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 登录" },
{ name: "description", content: "中国烟草AI合同及卷宗审核系统登录页面" },
];
};
// 加载器,获取当前会话状态
export async function loader({ request }: LoaderFunctionArgs) {
const { isAuthenticated } = await getUserSession(request);
// 如果已登录,重定向到首页
if (isAuthenticated) {
return redirect("/");
}
// 获取重定向URL并保存到session
const url = new URL(request.url);
const redirectTo = url.searchParams.get("redirect") || "/";
const session = await getSession(request);
session.set("redirectTo", redirectTo);
return Response.json({
isAuthenticated: false,
redirectTo
});
}
// 处理表单提交的action函数
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "test_user_login") {
// 获取重定向目标
const session = await getSession(request);
const redirectTo = session.get("redirectTo") || "/";
// 使用测试用户登录
const testUserSub = "001"; // 测试用户的sub
const userResult = await getUserBySub(testUserSub);
if (userResult.success && userResult.data) {
const user = userResult.data;
// 确保用户有默认角色
if (user.id) {
await addDefaultRole(user.id, 2); // 添加common角色
}
// 设置模拟的OAuth token信息
const mockTokenExpiresIn = 60 * 60 * 2; // 2小时,与真实OAuth token保持一致
const userRole = 'common';
// 生成前端专用JWT
const jwtUserInfo: UserInfoForJWT = {
sub: user.sub,
user_id: user.id!,
username: user.username,
nick_name: user.nick_name,
email: user.email,
phone_number: user.phone_number,
ou_id: user.ou_id,
ou_name: user.ou_name,
is_leader: user.is_leader,
user_role: userRole
};
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, mockTokenExpiresIn);
// 打印JWT生成信息
console.log("=== 测试用户登录 - JWT生成信息 ===");
console.log("用户信息:", jwtUserInfo);
console.log("生成的JWT:", frontendJWT);
console.log("JWT过期时间:", JWTUtils.getJWTExpiration(frontendJWT));
console.log("JWT解析结果:", JWTUtils.decodeJWT(frontendJWT));
console.log("JWT验证结果:", JWTUtils.verifyJWT(frontendJWT));
// 创建session,保持与OAuth登录相同的数据结构
session.set("isAuthenticated", true);
session.set("accessToken", "mock_access_token_for_test"); // 模拟的访问令牌
session.set("refreshToken", "mock_refresh_token_for_test"); // 模拟的刷新令牌
session.set("tokenIssuedAt", Date.now());
session.set("tokenExpiresIn", mockTokenExpiresIn);
session.set("userRole", userRole);
session.set("frontendJWT", frontendJWT);
// 构建与OAuth登录相同结构的userInfo
const enhancedUserInfo = {
// 保持与callback.tsx中相同的数据结构
sub: user.sub,
username: user.username,
nick_name: user.nick_name,
phone_number: user.phone_number,
email: user.email,
ou_id: user.ou_id,
ou_name: user.ou_name,
status: user.status,
is_leader: user.is_leader,
// 增强字段,与OAuth登录保持一致
user_id: user.id,
user_role: userRole,
frontend_jwt: frontendJWT
};
session.set("userInfo", enhancedUserInfo);
// 打印session信息
console.log("=== 测试用户登录 - Session信息 ===");
console.log("保存到session的userInfo:", enhancedUserInfo);
console.log("session数据结构:", {
isAuthenticated: true,
userRole: userRole,
accessToken: "mock_access_token_for_test",
refreshToken: "mock_refresh_token_for_test",
tokenIssuedAt: Date.now(),
tokenExpiresIn: mockTokenExpiresIn,
frontendJWT: frontendJWT,
userInfo: enhancedUserInfo
});
const cookie = await sessionStorage.commitSession(session);
console.log("=== 测试用户登录完成 ===");
console.log("用户:", user.username);
console.log("角色:", userRole);
console.log("重定向到:", redirectTo);
return new Response(null, {
status: 302,
headers: {
Location: redirectTo,
"Set-Cookie": cookie,
},
});
} else {
// 如果用户不存在,重定向到登录页面并显示错误
return redirect(`/login?error=${encodeURIComponent("测试用户不存在")}`);
}
}
return null;
}
export default function Login() {
const [searchParams] = useSearchParams();
const error = searchParams.get("error");
// 获取错误消息的友好描述
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 "登录回调处理失败,请重新登录";
default:
return decodeURIComponent(error);
}
};
// 处理OAuth2.0登录
const handleOAuthLogin = () => {
try {
// 创建OAuth客户端
const oauthClient = new OAuthClient(OAUTH_CONFIG);
// 生成状态值
const state = oauthClient.generateState();
// 将状态值保存到localStorage(用于后续验证)
localStorage.setItem("oauth_state", state);
// 获取授权URL
const authorizeUrl = oauthClient.getAuthorizeUrl(state);
// 重定向到IDaaS登录页面
window.location.href = authorizeUrl;
} catch (error) {
console.error("启动OAuth2.0登录失败:", error);
alert("登录系统初始化失败,请联系系统管理员");
}
};
useEffect(() => {
// 检查OAuth配置是否完整
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
}
}, []);
return (
<div className="login-page">
<div className="login-container">
<div className="login-header">
<h1 className="login-title">AI合同及卷宗审核系统</h1>
</div>
<div className="login-form-container">
<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>
)}
<div className="oauth-login-section">
<div className="login-description">
<p></p>
</div>
<button
onClick={handleOAuthLogin}
className="oauth-login-button"
type="button"
>
<i className="ri-shield-user-line"></i>
</button>
<div className="login-tips">
<p>
<i className="ri-information-line"></i>
</p>
</div>
</div>
{/* 测试用户登录区域 */}
<div className="temp-login-section">
<div className="section-divider">
<span></span>
</div>
<Form method="post" className="temp-login-form">
<input type="hidden" name="intent" value="test_user_login" />
<button
type="submit"
className="temp-admin-login-button"
>
<i className="ri-user-line"></i>
</button>
<div className="temp-login-tips">
<p>
<i className="ri-information-line"></i>
使(testuser1)
</p>
</div>
</Form>
</div>
</div>
<div className="login-footer">
<p>© 2025 </p>
</div>
</div>
</div>
);
}