Merge branch 'PingChuan' into shiy-login

# Conflicts:
#	app/config/api-config.ts
fix: 1. 修复无法加载数据的问题:没有从入口页中进来会缺少数据。
2. 加强后端接口关于token的校验错误和权限校验错误的管理。

feat: 1. 对接后端的数据看板的接口。
2. 将系统设置单独抽出来作为管理员的固定一个入口。
This commit is contained in:
2025-11-22 15:57:22 +08:00
27 changed files with 1972 additions and 643 deletions
+29 -132
View File
@@ -455,15 +455,27 @@ export async function logout(request: Request) {
const accessToken = session.get("accessToken");
const appId = OAUTH_CONFIG.appId || 'idaasoauth2';
// 如果存在访问令牌,调用IDaaS单点登出
console.log("🚪 [Logout] 开始登出流程...");
console.log("🔑 [Logout] accessToken 存在:", !!accessToken);
console.log("📱 [Logout] appId:", appId);
// 如果存在访问令牌,调用IDaaS单点登出(仅 OAuth 登录用户)
if (accessToken && appId) {
console.log("🌐 [Logout] OAuth 用户,准备调用 IDaaS 单点登出...");
try {
await callIDaaSLogout(accessToken, appId);
console.log("IDaaS单点登出成功");
console.log("✅ [Logout] IDaaS单点登出成功");
} catch (error) {
console.error("IDaaS单点登出失败:", error);
console.error("❌ [Logout] IDaaS单点登出失败:");
console.error(" 错误详情:", error);
if (error instanceof Error) {
console.error(" 错误消息:", error.message);
console.error(" 错误堆栈:", error.stack);
}
// 即使IDaaS登出失败,也继续清除本地会话
}
} else {
console.log("️ [Logout] 管理员登录用户,无需调用 IDaaS 登出");
}
return new Response(null, {
@@ -487,6 +499,11 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
const redirectUri = OAUTH_CONFIG.redirectUri || 'http://10.79.97.17/';
const logoutUrl = `${serverUrl}/public/sp/slo/${appId}`;
console.log("📡 [callIDaaSLogout] 准备发送登出请求:");
console.log(" 登出URL:", logoutUrl);
console.log(" 重定向URL:", redirectUri);
console.log(" accessToken:", accessToken ? `${accessToken.substring(0, 20)}...` : 'null');
const formData = new URLSearchParams();
formData.append('access_token', accessToken);
formData.append('redirect_url', encodeURIComponent(redirectUri));
@@ -498,13 +515,19 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
},
});
console.log("IDaaS单点登出请求成功");
console.log("✅ [callIDaaSLogout] IDaaS单点登出请求成功");
console.log(" 响应状态:", response.status);
console.log(" 响应数据:", response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("调用IDaaS登出接口失败:", error.response?.status, error.response?.statusText);
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败:");
console.error(" HTTP状态:", error.response?.status);
console.error(" 状态文本:", error.response?.statusText);
console.error(" 响应数据:", error.response?.data);
console.error(" 请求配置:", error.config?.url, error.config?.method);
throw new Error(`IDaaS登出失败: ${error.response?.status} ${error.response?.statusText}`);
}
console.error("调用IDaaS登出接口失败:", error);
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败(非HTTP错误):", error);
throw error;
}
}
@@ -751,129 +774,3 @@ export async function getUserBySub(sub: string) {
};
}
}
/**
* 账号密码登录接口
*
* @param username - 用户名
* @param password - 密码
* @param redirectTo - 登录成功后重定向的URL
* @returns HTTP重定向响应或错误响应
*/
export async function simpleRootLogin(
username: string,
password: string,
redirectTo: string
) {
try {
// 输入验证
if (!username?.trim() || !password?.trim()) {
return new Response(JSON.stringify({
success: false,
error: "用户名和密码不能为空"
}), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
// 调用登录接口
const loginResponse = await axios.post(`${API_BASE_URL}/password_login`, {
sub: username.trim(),
password: password.trim()
}, {
headers: {
'Content-Type': 'application/json',
}
});
const loginResult = loginResponse.data;
console.log('登录接口返回', loginResult);
// 检查重试次数
const retryCount = loginResult.retryCount || loginResult.retry_count || 0;
console.log('登录重试次数:', retryCount);
if (loginResult.code === 0 && loginResult.data) {
// 登录成功,构建用户信息
const userData = loginResult.data;
// console.log('管理员登录userData', userData);
const userRole = userData.role; // 默认角色
// 生成模拟的OAuth token信息
const mockTokenExpiresIn = 7200; // 2小时
const mockAccessToken = `mock_access_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const mockRefreshToken = `mock_refresh_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 生成前端JWT
const jwtUserInfo: UserInfoForJWT = {
sub: userData.sub,
user_id: userData.user_id,
username: userData.username,
nick_name: userData.nick_name,
email: userData.email,
phone_number: userData.phone_number,
ou_id: userData.ou_id,
ou_name: userData.ou_name,
is_leader: userData.is_leader,
user_role: userRole
};
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, mockTokenExpiresIn);
// 构建增强的用户信息对象
const enhancedUserInfo = {
...userData,
user_id: userData.user_id,
user_role: userRole,
frontend_jwt: frontendJWT
};
// 使用统一的session创建函数
return createUserSession({
isAuthenticated: true,
userRole: userRole,
redirectTo,
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
tokenExpiresIn: mockTokenExpiresIn,
userInfo: enhancedUserInfo,
frontendJWT
});
} else {
// 登录失败,检查账户是否被锁定
let errorMsg = loginResult.msg || "登录失败,请检查用户名和密码";
let isLocked = false;
// 检查是否因重试次数过多被锁定
if (retryCount >= 5) {
errorMsg = "账户已被锁定,密码错误次数过多,请联系管理员";
isLocked = true;
} else if (retryCount > 0) {
// 显示剩余尝试次数
const remainingAttempts = 5 - retryCount;
errorMsg = `${loginResult.msg || "用户名或密码错误"},还有 ${remainingAttempts} 次尝试机会`;
}
return new Response(JSON.stringify({
success: false,
error: errorMsg,
retryCount: retryCount,
isLocked: isLocked,
remainingAttempts: isLocked ? 0 : (5 - retryCount)
}), {
status: isLocked ? 403 : 401, // 403 表示禁止访问(账户被锁)
headers: { "Content-Type": "application/json" }
});
}
} catch (error) {
console.error("登录请求失败:", error);
return new Response(JSON.stringify({
success: false,
error: "登录请求失败,请稍后重试"
}), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
}