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

235 lines
8.8 KiB
TypeScript

import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
import { createUserSession, saveUserInfo, 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";
/**
* 端口号到地区的映射关系
* 根据 ecosystem.config.cjs 配置文件
*/
const PORT_TO_AREA_MAP: Record<string, string> = {
'51703': '梅州',
'51704': '云浮',
'51705': '揭阳',
'51706': '潮州',
'51707': '省局'
};
/**
* 根据端口号获取地区
* @param port - 端口号
* @returns 地区名称,如果未找到则返回 undefined
*/
function getAreaByPort(port: string): string | undefined {
return PORT_TO_AREA_MAP[port];
}
/**
* 辅助函数:使用 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")
const port = url.port; // 获取端口号
const area = getAreaByPort(port); // 根据端口号获取地区
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const error = url.searchParams.get("error");
const error_description = url.searchParams.get("error_description");
// console.log("🔧 OAuth2.0回调参数:", {
// code: code ? `${code.substring(0, 10)}...` : null,
// state: state,
// error: error,
// error_description: error_description,
// fullUrl: request.url,
// port: port,
// area: area
// });
// 检查是否有错误
if (error) {
console.error("❌ OAuth2.0授权失败:", error, error_description);
return redirectToLoginWithError(request, error_description || error || "授权失败");
}
// 检查是否有授权码
if (!code) {
console.error("❌ OAuth2.0回调缺少授权码");
return redirectToLoginWithError(request, "登录过程中缺少授权码,请重新登录");
}
// 验证状态值
if (!state || !state.endsWith("_idp")) {
console.error("❌ OAuth2.0状态值验证失败:", { state, expectedSuffix: "_idp" });
return redirectToLoginWithError(request, "登录状态验证失败,请重新登录");
}
console.log("✅ OAuth2.0回调参数验证通过");
// 声明在 try 外部,以便在 catch 中访问
let tokenResponse = null;
const oauthClient = new OAuthClient(getServerOAuthConfigRuntime());
try {
console.log("🔧 开始处理OAuth2.0回调");
// 开始获取访问令牌
tokenResponse = await oauthClient.getAccessToken(code);
if (!tokenResponse) {
console.error("获取访问令牌失败");
// 注意:此时还没有 access_token,无法调用 IDaaS 登出
// 只能重定向到登录页,让用户重新开始登录流程
return redirectToLoginWithError(request, "获取访问令牌失败,请重试");
}
console.log("✅ [Callback] 访问令牌获取成功");
const userInfo = await oauthClient.getUserInfo(tokenResponse.access_token);
if(!userInfo || !userInfo.success){
console.error('获取用户信息失败:',userInfo);
// 🔑 关键:此时 IDaaS 那边已经登录成功,但我们获取用户信息失败
// 需要调用 IDaaS 登出,清除 IDaaS 的登录状态,避免用户下次登录时出现问题
try {
const logoutUrl = `${url.protocol}//${url.host}/login`;
const logoutSuccess = await oauthClient.logout(tokenResponse.access_token, logoutUrl);
if (logoutSuccess) {
console.log("✅ [Callback] 已清除 IDaaS 登录状态");
} else {
console.warn("⚠️ [Callback] 清除 IDaaS 登录状态失败");
}
} catch (logoutError) {
console.error("❌ [Callback] 调用 IDaaS 登出时出错:", logoutError);
}
return redirectToLoginWithError(request, "获取用户信息失败,请重试");
}
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);
// 🔑 保存用户信息失败,需要清除 IDaaS 登录状态
try {
const logoutUrl = `${url.protocol}//${url.host}/login`;
const logoutSuccess = await oauthClient.logout(tokenResponse.access_token, logoutUrl);
if (logoutSuccess) {
console.log("✅ [Callback] 已清除 IDaaS 登录状态(数据库保存失败)");
} else {
console.warn("⚠️ [Callback] 清除 IDaaS 登录状态失败(数据库保存失败)");
}
} catch (logoutError) {
console.error("❌ [Callback] 调用 IDaaS 登出时出错(数据库保存失败):", logoutError);
}
return redirectToLoginWithError(request, "保存用户信息失败,请重新登录");
}
console.log("用户信息已成功保存到数据库,地区:", area || "未设置");
const savedUserData = saveResult.data!;
// 生成前端专用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 等存在
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,
frontend_jwt: frontendJWT
};
// 使用统一的session创建函数
return createUserSession({
isAuthenticated: true,
userRole: userRole,
redirectTo,
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
tokenExpiresIn: tokenResponse.expires_in,
userInfo: enhancedUserInfo,
frontendJWT
});
} catch (error) {
console.error("OAuth2.0回调处理失败:", error);
// 🔑 如果已经获取到了 access_token,需要清除 IDaaS 登录状态
if (tokenResponse?.access_token) {
try {
const logoutUrl = `${url.protocol}//${url.host}/login`;
const logoutSuccess = await oauthClient.logout(tokenResponse.access_token, logoutUrl);
if (logoutSuccess) {
console.log("✅ [Callback] 已清除 IDaaS 登录状态(回调处理异常)");
} else {
console.warn("⚠️ [Callback] 清除 IDaaS 登录状态失败(回调处理异常)");
}
} catch (logoutError) {
console.error("❌ [Callback] 调用 IDaaS 登出时出错(回调处理异常):", logoutError);
}
}
return redirectToLoginWithError(request, "登录回调处理失败,请重新登录");
}
}
export default function Callback() {
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>
</div>
</div>
);
}