feat: 1. 添加axios全局路由拦截进行自动添加请求jwt。 2.重新整理路由表。 3. 文档列表新增版本差异对比。 4.菜单路由可访问列表通过对接接口返回,添加全局路由检测。
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
This commit is contained in:
+115
-53
@@ -1,8 +1,10 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { createUserSession, saveUserInfo, sessionStorage } from "~/api/login/auth.server";
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "@remix-run/react";
|
||||
import { createUserSession, sessionStorage } from "~/api/login/auth.server";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
import { loginWithOAuth, type LoginRequest } from "~/api/login/login-client";
|
||||
|
||||
/**
|
||||
* 端口号到地区的映射关系
|
||||
@@ -49,6 +51,30 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const error = url.searchParams.get("error");
|
||||
const error_description = url.searchParams.get("error_description");
|
||||
|
||||
// 🔑 检查是否是管理员账密登录(直接传递 token 和 userInfo)
|
||||
const token = url.searchParams.get("token");
|
||||
const userInfo = url.searchParams.get("userInfo");
|
||||
const redirectTo = url.searchParams.get("redirectTo") || "/";
|
||||
|
||||
// 🔑 如果有 token 和 userInfo,说明是管理员账密登录
|
||||
// login.tsx action 已经创建了 Cookie Session,这里只需要返回 null
|
||||
// 让客户端组件从 URL 读取 token 并保存到 localStorage
|
||||
if (token && userInfo) {
|
||||
console.log("✅ [Callback] 检测到管理员账密登录,交给客户端处理");
|
||||
|
||||
// 验证 userInfo 格式(防止解析错误)
|
||||
try {
|
||||
JSON.parse(decodeURIComponent(userInfo));
|
||||
} catch (error) {
|
||||
console.error("❌ [Callback] 解析管理员登录用户信息失败:", error);
|
||||
return redirectToLoginWithError(request, "登录失败:用户信息格式错误");
|
||||
}
|
||||
|
||||
// ✅ 直接返回 null,让客户端 useEffect 处理
|
||||
// 注意:Cookie Session 已经在 login.tsx action 中通过 createUserSession 创建
|
||||
return null;
|
||||
}
|
||||
|
||||
// console.log("🔧 OAuth2.0回调参数:", {
|
||||
// code: code ? `${code.substring(0, 10)}...` : null,
|
||||
// state: state,
|
||||
@@ -119,81 +145,79 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
console.log("✅ [Callback] 用户信息获取成功");
|
||||
|
||||
|
||||
// TODO 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 暂定都是common
|
||||
const userRole = "common";
|
||||
|
||||
// 获取重定向URL
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
// 🔒 安全:临时 JWT 现在在 saveUserInfo() 内部生成,避免在客户端代码中暴露 user_id 逻辑
|
||||
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
|
||||
const saveResult = await saveUserInfo(
|
||||
userInfo.data,
|
||||
userRole,
|
||||
tokenResponse.expires_in,
|
||||
area
|
||||
);
|
||||
if (!saveResult.success) {
|
||||
console.error("保存用户信息到数据库失败:", saveResult.error);
|
||||
// 调用后端登录接口,传递 OAuth 用户信息,获取 JWT token
|
||||
const loginRequest: LoginRequest = {
|
||||
userInfo: userInfo.data,
|
||||
expiresIn: tokenResponse.expires_in,
|
||||
area: area
|
||||
};
|
||||
|
||||
// 🔑 保存用户信息失败,需要清除 IDaaS 登录状态
|
||||
const loginResponse = await loginWithOAuth(loginRequest);
|
||||
|
||||
if (!loginResponse.success || !loginResponse.data) {
|
||||
console.error("❌ [Callback] 后端登录失败:", loginResponse.error);
|
||||
|
||||
// 🔑 登录失败,需要清除 IDaaS 登录状态
|
||||
try {
|
||||
const logoutUrl = `${url.protocol}//${url.host}/login`;
|
||||
const logoutSuccess = await oauthClient.logout(tokenResponse.access_token, logoutUrl);
|
||||
if (logoutSuccess) {
|
||||
console.log("✅ [Callback] 已清除 IDaaS 登录状态(数据库保存失败)");
|
||||
console.log("✅ [Callback] 已清除 IDaaS 登录状态(后端登录失败)");
|
||||
} else {
|
||||
console.warn("⚠️ [Callback] 清除 IDaaS 登录状态失败(数据库保存失败)");
|
||||
console.warn("⚠️ [Callback] 清除 IDaaS 登录状态失败(后端登录失败)");
|
||||
}
|
||||
} catch (logoutError) {
|
||||
console.error("❌ [Callback] 调用 IDaaS 登出时出错(数据库保存失败):", logoutError);
|
||||
console.error("❌ [Callback] 调用 IDaaS 登出时出错(后端登录失败):", logoutError);
|
||||
}
|
||||
|
||||
return redirectToLoginWithError(request, "保存用户信息失败,请重新登录");
|
||||
return redirectToLoginWithError(request, loginResponse.error || "登录失败,请重新登录");
|
||||
}
|
||||
|
||||
console.log("用户信息已成功保存到数据库,地区:", area || "未设置");
|
||||
const savedUserData = saveResult.data!;
|
||||
console.log("✅ [Callback] 后端登录成功,JWT token 已获取");
|
||||
const frontendJWT = loginResponse.data.access_token;
|
||||
const savedUserInfo = loginResponse.data.user_info;
|
||||
|
||||
// 生成前端专用JWT(使用完整的用户信息,包括数据库 ID)
|
||||
const jwtUserInfo: UserInfoForJWT = {
|
||||
sub: userInfo.data.sub,
|
||||
user_id: savedUserData.id!,
|
||||
username: savedUserData.username,
|
||||
nick_name: savedUserData.nick_name,
|
||||
email: savedUserData.email,
|
||||
phone_number: savedUserData.phone_number,
|
||||
ou_id: savedUserData.ou_id,
|
||||
ou_name: savedUserData.ou_name,
|
||||
is_leader: savedUserData.is_leader,
|
||||
user_role: userRole as 'common' | 'developer'
|
||||
};
|
||||
|
||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
||||
// console.log("前端JWT已生成");
|
||||
|
||||
// 更新userInfo以包含数据库ID、JWT,并用数据库标准字段覆盖关键属性,确保 nick_name 等存在
|
||||
// 更新userInfo以包含数据库ID、JWT(user_role 从后端返回)
|
||||
const enhancedUserInfo = {
|
||||
...userInfo.data, // 保留OAuth返回的原字段(包含 nickname 等)
|
||||
username: savedUserData.username,
|
||||
nick_name: savedUserData.nick_name,
|
||||
phone_number: savedUserData.phone_number,
|
||||
email: savedUserData.email,
|
||||
ou_id: savedUserData.ou_id,
|
||||
ou_name: savedUserData.ou_name,
|
||||
status: savedUserData.status,
|
||||
is_leader: savedUserData.is_leader,
|
||||
user_id: savedUserData.id,
|
||||
user_role: userRole,
|
||||
username: savedUserInfo.username,
|
||||
nick_name: savedUserInfo.nick_name,
|
||||
phone_number: savedUserInfo.phone_number,
|
||||
email: savedUserInfo.email,
|
||||
ou_id: savedUserInfo.ou_id,
|
||||
ou_name: savedUserInfo.ou_name,
|
||||
is_leader: savedUserInfo.is_leader,
|
||||
user_id: savedUserInfo.user_id,
|
||||
user_role: savedUserInfo.user_role, // 使用后端返回的角色
|
||||
frontend_jwt: frontendJWT
|
||||
};
|
||||
|
||||
// 🔑 重要:将 token 和用户信息作为 URL 参数传递给客户端
|
||||
// 客户端 useEffect 会将其保存到 localStorage
|
||||
const callbackUrl = new URL('/callback', url.origin);
|
||||
callbackUrl.searchParams.set('token', frontendJWT);
|
||||
callbackUrl.searchParams.set('userInfo', encodeURIComponent(JSON.stringify({
|
||||
user_id: savedUserInfo.user_id,
|
||||
username: savedUserInfo.username,
|
||||
nick_name: savedUserInfo.nick_name,
|
||||
email: savedUserInfo.email,
|
||||
phone_number: savedUserInfo.phone_number,
|
||||
ou_id: savedUserInfo.ou_id,
|
||||
ou_name: savedUserInfo.ou_name,
|
||||
is_leader: savedUserInfo.is_leader,
|
||||
user_role: savedUserInfo.user_role,
|
||||
sub: userInfo.data.sub
|
||||
})));
|
||||
callbackUrl.searchParams.set('redirectTo', redirectTo);
|
||||
|
||||
// 使用统一的session创建函数
|
||||
return createUserSession({
|
||||
isAuthenticated: true,
|
||||
userRole: userRole,
|
||||
redirectTo,
|
||||
userRole: savedUserInfo.user_role, // 使用后端返回的角色
|
||||
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
tokenExpiresIn: tokenResponse.expires_in,
|
||||
@@ -224,11 +248,49 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function Callback() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// 从 URL 参数中获取 token(如果有)
|
||||
const token = searchParams.get("token");
|
||||
const userInfo = searchParams.get("userInfo");
|
||||
const redirectTo = searchParams.get("redirectTo") || "/";
|
||||
|
||||
if (token && typeof window !== 'undefined') {
|
||||
console.log('🔑 [Callback] 开始保存 token 到 localStorage');
|
||||
|
||||
// 存储 token 到 localStorage
|
||||
localStorage.setItem('access_token', token);
|
||||
console.log('✅ [Callback] Token 已存储到 localStorage');
|
||||
|
||||
// 存储用户信息
|
||||
if (userInfo) {
|
||||
try {
|
||||
const parsedUserInfo = JSON.parse(decodeURIComponent(userInfo));
|
||||
localStorage.setItem('user_info', JSON.stringify(parsedUserInfo));
|
||||
console.log('✅ [Callback] 用户信息已存储到 localStorage:', parsedUserInfo);
|
||||
} catch (error) {
|
||||
console.error('❌ [Callback] 解析用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ⏱️ 短暂延迟后跳转,确保 localStorage 写入完成
|
||||
// 使用 requestAnimationFrame 确保 DOM 更新后立即跳转
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
console.log(`🚀 [Callback] 跳转到目标页面: ${redirectTo}`);
|
||||
window.location.href = redirectTo;
|
||||
}, 100); // 减少延迟从 500ms 到 100ms
|
||||
});
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">正在处理登录...</p>
|
||||
<p className="mt-2 text-sm text-gray-500">即将跳转...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user