添加管理员登陆,添加nginx反向代理配置,
This commit is contained in:
+230
-59
@@ -21,6 +21,7 @@ import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { tokenManager } from "./token-manager.server";
|
||||
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
|
||||
|
||||
/**
|
||||
* 用户角色类型定义
|
||||
@@ -289,11 +290,11 @@ export async function getUserSession(request: Request) {
|
||||
|
||||
// 打印JWT重新生成信息
|
||||
console.log("=== Token刷新时重新生成JWT ===");
|
||||
console.log("原始userInfo:", userInfo);
|
||||
console.log("重构的用户数据:", mockSavedUserData);
|
||||
console.log("用户角色:", userRole);
|
||||
console.log("新生成的JWT:", newJWT);
|
||||
console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
|
||||
// console.log("原始userInfo:", userInfo);
|
||||
// console.log("重构的用户数据:", mockSavedUserData);
|
||||
// console.log("用户角色:", userRole);
|
||||
// console.log("新生成的JWT:", newJWT);
|
||||
// console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
|
||||
|
||||
// 更新session中的JWT
|
||||
if (!refreshedSession) {
|
||||
@@ -329,13 +330,79 @@ export async function getUserSession(request: Request) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户登录会话
|
||||
* 创建用户登录会话(完整版)
|
||||
*
|
||||
* 在用户成功登录后调用此函数来创建完整的会话并设置 Cookie。
|
||||
* 这个函数支持完整的OAuth2.0登录流程,包括token管理和用户信息存储。
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 创建新的会话对象
|
||||
* 2. 设置认证状态、用户角色、token信息
|
||||
* 3. 保存用户信息和前端JWT
|
||||
* 4. 生成签名的 Cookie
|
||||
* 5. 返回重定向响应并设置 Cookie
|
||||
*
|
||||
* @param params - 会话创建参数
|
||||
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
|
||||
*/
|
||||
export async function createUserSession(params: {
|
||||
isAuthenticated: boolean;
|
||||
userRole: UserRole;
|
||||
redirectTo: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
tokenExpiresIn?: number;
|
||||
userInfo?: UserInfo;
|
||||
frontendJWT?: string;
|
||||
}) {
|
||||
const session = await sessionStorage.getSession();
|
||||
|
||||
// 基础认证信息
|
||||
session.set("isAuthenticated", params.isAuthenticated);
|
||||
session.set("userRole", params.userRole);
|
||||
|
||||
// OAuth token信息
|
||||
if (params.accessToken) {
|
||||
session.set("accessToken", params.accessToken);
|
||||
session.set("tokenIssuedAt", Date.now());
|
||||
}
|
||||
if (params.refreshToken) {
|
||||
session.set("refreshToken", params.refreshToken);
|
||||
}
|
||||
if (params.tokenExpiresIn) {
|
||||
session.set("tokenExpiresIn", params.tokenExpiresIn);
|
||||
}
|
||||
|
||||
// 用户信息和JWT
|
||||
if (params.userInfo) {
|
||||
session.set("userInfo", params.userInfo);
|
||||
}
|
||||
if (params.frontendJWT) {
|
||||
session.set("frontendJWT", params.frontendJWT);
|
||||
}
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
// console.log("创建完整会话 - 设置Cookie:", !!cookie);
|
||||
// console.log("创建完整会话 - 用户角色:", params.userRole);
|
||||
// console.log("创建完整会话 - 重定向到:", params.redirectTo);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302, // HTTP 重定向状态码
|
||||
headers: {
|
||||
Location: params.redirectTo, // 重定向目标 URL
|
||||
"Set-Cookie": cookie, // 设置会话 Cookie
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户登录会话(简化版)
|
||||
*
|
||||
* 在用户成功登录后调用此函数来创建会话并设置 Cookie。
|
||||
* 这个函数通常在以下场景中使用:
|
||||
* - OAuth2.0 登录成功后
|
||||
* - 临时管理员登录
|
||||
* - 其他认证方式成功后
|
||||
* - 测试用户登录
|
||||
* - 其他简单认证方式成功后
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 创建新的会话对象
|
||||
@@ -348,15 +415,15 @@ export async function getUserSession(request: Request) {
|
||||
* @param redirectTo - 登录成功后重定向的 URL,默认为首页
|
||||
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
|
||||
*/
|
||||
export async function createUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
|
||||
export async function createSimpleUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", isAuthenticated);
|
||||
session.set("userRole", userRole);
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
console.log("创建会话 - 设置Cookie:", !!cookie);
|
||||
console.log("创建会话 - 用户角色:", userRole);
|
||||
console.log("创建会话 - 重定向到:", redirectTo);
|
||||
console.log("创建简化会话 - 设置Cookie:", !!cookie);
|
||||
console.log("创建简化会话 - 用户角色:", userRole);
|
||||
console.log("创建简化会话 - 重定向到:", redirectTo);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302, // HTTP 重定向状态码
|
||||
@@ -373,14 +440,15 @@ export async function createUserSession(isAuthenticated: boolean, userRole: User
|
||||
* 当用户主动登出或会话失效时调用此函数。
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 获取当前用户的会话
|
||||
* 2. 销毁会话数据(清除所有存储的信息)
|
||||
* 3. 清除客户端的会话 Cookie
|
||||
* 4. 重定向到登录页面
|
||||
* 1. 获取当前用户的会话和访问令牌
|
||||
* 2. 调用 IDaaS 单点登出接口
|
||||
* 3. 销毁本地会话数据(清除所有存储的信息)
|
||||
* 4. 清除客户端的会话 Cookie
|
||||
* 5. 重定向到登录页面
|
||||
*
|
||||
* 注意事项:
|
||||
* - 这个函数只处理本地会话,不会调用 IDaaS 的单点登出
|
||||
* - 如果需要全局登出,应该额外调用 IDaaS 的 SLO 接口
|
||||
* - 这个函数会同时处理本地会话和 IDaaS 的单点登出
|
||||
* - 即使 IDaaS 登出失败,也会清除本地会话
|
||||
* - 销毁会话后,用户需要重新登录才能访问受保护的页面
|
||||
*
|
||||
* @param request - Remix Request 对象,用于获取当前会话
|
||||
@@ -388,6 +456,21 @@ export async function createUserSession(isAuthenticated: boolean, userRole: User
|
||||
*/
|
||||
export async function logout(request: Request) {
|
||||
const session = await getSession(request);
|
||||
|
||||
// 获取访问令牌和应用ID,用于调用IDaaS单点登出
|
||||
const accessToken = session.get("accessToken");
|
||||
const appId = OAUTH_CONFIG.appId;
|
||||
|
||||
// 如果存在访问令牌,调用IDaaS单点登出
|
||||
if (accessToken && appId) {
|
||||
try {
|
||||
await callIDaaSLogout(accessToken, appId);
|
||||
console.log("IDaaS单点登出成功");
|
||||
} catch (error) {
|
||||
console.error("IDaaS单点登出失败:", error);
|
||||
// 即使IDaaS登出失败,也继续清除本地会话
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(null, {
|
||||
status: 302, // HTTP 重定向状态码
|
||||
@@ -398,6 +481,40 @@ export async function logout(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用IDaaS单点登出接口
|
||||
*
|
||||
* @param accessToken - 用户的访问令牌
|
||||
* @param appId - 应用ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async function callIDaaSLogout(accessToken: string, appId: string): Promise<void> {
|
||||
const logoutUrl = `${OAUTH_CONFIG.serverUrl}/public/sp/slo/${appId}`;
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('access_token', accessToken);
|
||||
formData.append('redirect_url', encodeURIComponent(OAUTH_CONFIG.redirectUri));
|
||||
|
||||
try {
|
||||
const response = await fetch(logoutUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`IDaaS登出失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
console.log("IDaaS单点登出请求成功");
|
||||
} catch (error) {
|
||||
console.error("调用IDaaS登出接口失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息到数据库
|
||||
*
|
||||
@@ -564,7 +681,7 @@ export async function addDefaultRole(userId: string, roleId: number = 2) {
|
||||
*/
|
||||
export async function getUserBySub(sub: string) {
|
||||
try {
|
||||
console.log(`查询用户: ${sub}`);
|
||||
// console.log(`查询用户: ${sub}`);
|
||||
|
||||
const userResult = await postgrestGet<SsoUser[]>("sso_users", {
|
||||
filter: {
|
||||
@@ -595,49 +712,103 @@ export async function getUserBySub(sub: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户登录会话(支持用户信息)
|
||||
* 账号密码登录接口
|
||||
*
|
||||
* @param isAuthenticated - 是否已认证
|
||||
* @param userRole - 用户角色
|
||||
* @param redirectTo - 重定向URL
|
||||
* @param userInfo - 可选的用户信息
|
||||
* @returns HTTP重定向响应
|
||||
* @param username - 用户名
|
||||
* @param password - 密码
|
||||
* @param redirectTo - 登录成功后重定向的URL
|
||||
* @returns HTTP重定向响应或错误响应
|
||||
*/
|
||||
export async function createUserSessionWithInfo(
|
||||
isAuthenticated: boolean,
|
||||
userRole: UserRole,
|
||||
redirectTo: string,
|
||||
userInfo?: Partial<SsoUser>
|
||||
export async function simpleRootLogin(
|
||||
username: string,
|
||||
password: string,
|
||||
redirectTo: string
|
||||
) {
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", isAuthenticated);
|
||||
session.set("userRole", userRole);
|
||||
|
||||
// 如果提供了用户信息,也保存到session中
|
||||
if (userInfo) {
|
||||
session.set("userInfo", {
|
||||
sub: userInfo.sub,
|
||||
user_id: userInfo.id,
|
||||
username: userInfo.username,
|
||||
nick_name: userInfo.nick_name,
|
||||
email: userInfo.email,
|
||||
ou_name: userInfo.ou_name,
|
||||
is_leader: userInfo.is_leader,
|
||||
user_role: userRole
|
||||
try {
|
||||
// 输入验证
|
||||
if (!username?.trim() || !password?.trim()) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: "用户名和密码不能为空"
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 调用登录接口
|
||||
const loginResponse = await fetch(`${API_BASE_URL}/password_login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sub: username.trim(),
|
||||
password: password.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const loginResult = await loginResponse.json();
|
||||
|
||||
if (loginResult.code === 0 && loginResult.data) {
|
||||
// 登录成功,构建用户信息
|
||||
const userData = loginResult.data;
|
||||
const userRole = 'common' as UserRole; // 默认角色
|
||||
|
||||
// 构建用户信息对象
|
||||
const userInfo = {
|
||||
sub: userData.sub,
|
||||
user_id: userData.sub, // 使用sub作为user_id
|
||||
username: userData.username,
|
||||
nick_name: userData.nick_name,
|
||||
phone_number: userData.phone_number,
|
||||
email: userData.email,
|
||||
ou_id: userData.ou_id,
|
||||
ou_name: userData.ou_name,
|
||||
is_leader: userData.is_leader,
|
||||
user_role: userRole
|
||||
};
|
||||
|
||||
// 创建会话
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", true);
|
||||
session.set("userRole", userRole);
|
||||
session.set("userInfo", userInfo);
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
|
||||
// console.log("账号密码登录成功 - 用户:", userData.username);
|
||||
// console.log("账号密码登录成功 - 角色:", userRole);
|
||||
// console.log("账号密码登录成功 - 重定向到:", redirectTo);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: redirectTo,
|
||||
"Set-Cookie": cookie,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 登录失败,返回错误信息
|
||||
const errorMsg = loginResult.msg || "登录失败,请检查用户名和密码";
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: errorMsg
|
||||
}), {
|
||||
status: 401,
|
||||
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" }
|
||||
});
|
||||
}
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
console.log("创建会话 - 设置Cookie:", !!cookie);
|
||||
console.log("创建会话 - 用户角色:", userRole);
|
||||
console.log("创建会话 - 用户信息:", userInfo?.username || "无");
|
||||
console.log("创建会话 - 重定向到:", redirectTo);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: redirectTo,
|
||||
"Set-Cookie": cookie,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -200,9 +200,16 @@ export class OAuthClient {
|
||||
* @returns 状态值字符串
|
||||
*/
|
||||
generateState(): string {
|
||||
// 获取当前端口号,优先级:API_PORT_CONFIG > PORT > 默认值
|
||||
const currentPort = process.env.API_PORT_CONFIG || process.env.PORT;
|
||||
|
||||
const randomStr = Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15);
|
||||
return `${randomStr}_idp`;
|
||||
|
||||
const stateValue = `login${currentPort}_${randomStr}_idp`;
|
||||
console.log(`生成状态值: ${stateValue} (端口: ${currentPort})`);
|
||||
|
||||
return stateValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,8 +212,25 @@ export function Sidebar({ onToggle, collapsed, userRole, selectedApp = '' }: Sid
|
||||
// const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP]
|
||||
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
|
||||
|
||||
// 检查是否通过51708端口访问
|
||||
// const isPort51708 = typeof window !== 'undefined' && window.location.port === '51708';
|
||||
const isPort51708 = typeof window !== 'undefined' && window.location.port === '5178';
|
||||
|
||||
// 根据当前应用模式过滤菜单项
|
||||
const filteredMenuItems = menuItems.filter(item => {
|
||||
// 如果是51708端口,只显示交叉评查相关菜单
|
||||
if (isPort51708) {
|
||||
// 如果当前应用是智慧法务大模型,只显示AI对话菜单
|
||||
if (currentApp === 'model') {
|
||||
return item.id === 'chat-with-llm' ||
|
||||
(item.path && item.path.startsWith('/chat-with-llm'));
|
||||
}else{
|
||||
return item.id === 'cross-checking' ||
|
||||
(item.path && item.path.startsWith('/cross-checking'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 检查当前菜单是否在所选应用模式中显示
|
||||
if (!visibleMenuIds.includes(item.id)) {
|
||||
return false;
|
||||
|
||||
+170
-25
@@ -30,31 +30,82 @@ interface ApiConfig {
|
||||
// 端口特定配置映射
|
||||
// 根据不同端口提供不同的API配置
|
||||
const portConfigs: Record<string, Partial<ApiConfig>> = {
|
||||
|
||||
// 测试主要服务实例
|
||||
'5173': {
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8008/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5174': {
|
||||
baseUrl: 'http://172.16.0.55:5174',
|
||||
documentUrl: 'http://172.16.0.55:5174/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5174/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5175': {
|
||||
baseUrl: 'http://172.16.0.55:5175',
|
||||
documentUrl: 'http://172.16.0.55:5175/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5175/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5176': {
|
||||
baseUrl: 'http://172.16.0.55:5176',
|
||||
documentUrl: 'http://172.16.0.55:5176/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5176/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5177': {
|
||||
baseUrl: 'http://172.16.0.55:5177',
|
||||
documentUrl: 'http://172.16.0.55:5177/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5177/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5178': {
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8008/admin/documents'
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 主要
|
||||
'51703': {
|
||||
baseUrl: 'http://172.16.0.55:51703',
|
||||
documentUrl: 'http://172.16.0.55:51703/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:51703/admin/documents'
|
||||
},
|
||||
|
||||
// 潮州
|
||||
'51704': {
|
||||
baseUrl: 'http://172.16.0.55:51704',
|
||||
documentUrl: 'http://172.16.0.55:51704/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:51704/admin/documents'
|
||||
},
|
||||
|
||||
// 揭阳
|
||||
'51705': {
|
||||
baseUrl: 'http://172.16.0.55:51705',
|
||||
documentUrl: 'http://172.16.0.55:51705/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:51705/admin/documents'
|
||||
},
|
||||
|
||||
// 云浮
|
||||
'51706': {
|
||||
baseUrl: 'http://172.16.0.55:51706',
|
||||
documentUrl: 'http://172.16.0.55:51706/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:51706/admin/documents'
|
||||
},
|
||||
|
||||
// 梅州
|
||||
'51707': {
|
||||
baseUrl: 'http://172.16.0.55:51707',
|
||||
documentUrl: 'http://172.16.0.55:51707/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:51707/admin/documents'
|
||||
},
|
||||
|
||||
// 省局
|
||||
'51708': {
|
||||
baseUrl: 'http://172.16.0.55:51708',
|
||||
documentUrl: 'http://172.16.0.55:51708/docauditai/',
|
||||
@@ -86,14 +137,19 @@ const configs: Record<string, ApiConfig> = {
|
||||
|
||||
// 测试环境
|
||||
testing: {
|
||||
baseUrl: 'http://nas.7bm.co:3000',
|
||||
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload',
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
// baseUrl: 'http://172.16.0.81:3000',
|
||||
// baseUrl: 'http://nas.7bm.co:3000',
|
||||
// documentUrl: 'http://172.16.0.81:9000/docauditai/',
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8008/admin/documents',
|
||||
// uploadUrl: 'http://172.16.0.58:8008/admin/documents',
|
||||
// uploadUrl: 'http://172.16.0.58:8008/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', // 需要替换为实际的Client ID
|
||||
clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://nas.7bm.co:3000/callback', // 回调地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
},
|
||||
@@ -101,8 +157,8 @@ const configs: Record<string, ApiConfig> = {
|
||||
// 生产环境
|
||||
production: {
|
||||
// postgrest
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
// baseUrl: 'http://10.79.97.17:8000',
|
||||
// baseUrl: 'http://172.16.0.55:8008',
|
||||
baseUrl: 'http://10.79.97.17:8000',
|
||||
// minio
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
// 文件上传
|
||||
@@ -133,8 +189,31 @@ const configs: Record<string, ApiConfig> = {
|
||||
|
||||
// 获取当前环境,默认为development
|
||||
const getCurrentEnvironment = (): string => {
|
||||
// 优先使用环境变量,然后使用 NODE_ENV
|
||||
return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development';
|
||||
// 在服务器端,优先使用PM2设置的环境变量
|
||||
if (typeof window === 'undefined') {
|
||||
// 服务器端:直接使用process.env.NODE_ENV
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
console.log('🔧 服务器端环境检测:', {
|
||||
NODE_ENV: nodeEnv,
|
||||
result: nodeEnv || 'development'
|
||||
});
|
||||
return nodeEnv || 'development';
|
||||
}
|
||||
|
||||
// 客户端:优先使用NEXT_PUBLIC_前缀的环境变量
|
||||
const nextPublicNodeEnv = process.env.NEXT_PUBLIC_NODE_ENV;
|
||||
const nextPublicEnv = process.env.NEXT_PUBLIC_API_ENV;
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
const result = nextPublicNodeEnv || nextPublicEnv || nodeEnv || 'development';
|
||||
|
||||
console.log('🔧 客户端环境检测:', {
|
||||
NEXT_PUBLIC_NODE_ENV: nextPublicNodeEnv,
|
||||
NEXT_PUBLIC_API_ENV: nextPublicEnv,
|
||||
NODE_ENV: nodeEnv,
|
||||
result: result
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 从环境变量获取配置,如果环境变量不存在则使用默认配置
|
||||
@@ -155,20 +234,67 @@ const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
|
||||
|
||||
/**
|
||||
* 获取当前端口号
|
||||
* 优先从环境变量获取,然后从浏览器location获取
|
||||
* 优先从浏览器location获取,然后从环境变量获取
|
||||
*/
|
||||
const getCurrentPort = (): string => {
|
||||
// 在客户端,优先从浏览器location获取端口
|
||||
let windowPort = '';
|
||||
if (typeof window !== 'undefined') {
|
||||
windowPort = window.location.port || '';
|
||||
}
|
||||
|
||||
// 在服务器端,优先使用运行时端口检测
|
||||
if (typeof window === 'undefined') {
|
||||
const runtimePort = getRuntimePort();
|
||||
if (runtimePort) {
|
||||
console.log('🔧 服务器端运行时端口检测:', runtimePort);
|
||||
return runtimePort;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用环境变量中的端口配置
|
||||
if (process.env.API_PORT_CONFIG) {
|
||||
return process.env.API_PORT_CONFIG;
|
||||
const nextPublicApiPortConfig = process.env.NEXT_PUBLIC_API_PORT_CONFIG;
|
||||
const nextPublicPort = process.env.NEXT_PUBLIC_PORT;
|
||||
const apiPortConfig = process.env.API_PORT_CONFIG;
|
||||
const portEnv = process.env.PORT;
|
||||
|
||||
// 优先级:windowPort > NEXT_PUBLIC_API_PORT_CONFIG > NEXT_PUBLIC_PORT > API_PORT_CONFIG > PORT环境变量
|
||||
const result = windowPort || nextPublicApiPortConfig || nextPublicPort || apiPortConfig || portEnv || '';
|
||||
|
||||
console.log('🔧 端口检测:', {
|
||||
windowPort: windowPort,
|
||||
NEXT_PUBLIC_API_PORT_CONFIG: nextPublicApiPortConfig,
|
||||
NEXT_PUBLIC_PORT: nextPublicPort,
|
||||
API_PORT_CONFIG: apiPortConfig,
|
||||
PORT: portEnv,
|
||||
result: result
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 运行时端口检测 - 从服务器启动参数或环境变量获取实际端口
|
||||
* 这个方法只在服务器端运行,用于动态获取实际运行端口
|
||||
*/
|
||||
const getRuntimePort = (): string => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return ''; // 客户端不执行此逻辑
|
||||
}
|
||||
|
||||
// 如果是浏览器环境,从location获取端口
|
||||
if (typeof window !== 'undefined' && window.location.port) {
|
||||
return window.location.port;
|
||||
// 尝试从进程参数中获取端口
|
||||
const args = process.argv;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--port' && i + 1 < args.length) {
|
||||
return args[i + 1];
|
||||
}
|
||||
if (args[i].startsWith('--port=')) {
|
||||
return args[i].split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
// 从环境变量获取
|
||||
return process.env.PORT || '';
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -179,24 +305,40 @@ const getCurrentConfig = (): ApiConfig => {
|
||||
const env = getCurrentEnvironment();
|
||||
const port = getCurrentPort();
|
||||
|
||||
console.log('🔧 配置调试信息:', {
|
||||
environment: env,
|
||||
port: port,
|
||||
hasPortConfig: !!(port && portConfigs[port]),
|
||||
portConfig: port ? portConfigs[port] : null
|
||||
});
|
||||
|
||||
// 获取基础配置
|
||||
let defaultConfig = configs[env] || configs.development;
|
||||
|
||||
// 如果有端口特定配置,则合并配置
|
||||
if (port && portConfigs[port]) {
|
||||
console.log(`🔧 使用端口特定配置: ${port}`, portConfigs[port]);
|
||||
defaultConfig = {
|
||||
...defaultConfig,
|
||||
...portConfigs[port],
|
||||
// 保持oauth配置不变,只覆盖API相关配置
|
||||
oauth: defaultConfig.oauth
|
||||
};
|
||||
} else {
|
||||
console.log(`🔧 使用环境配置: ${env}`, defaultConfig);
|
||||
}
|
||||
|
||||
// 如果是浏览器环境,尝试从环境变量覆盖配置
|
||||
if (typeof window !== 'undefined' || process.env.NEXT_PUBLIC_API_BASE_URL) {
|
||||
// 只有在明确设置了环境变量的情况下才覆盖配置
|
||||
const hasEnvOverrides = process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_DOCUMENT_URL ||
|
||||
process.env.NEXT_PUBLIC_UPLOAD_URL;
|
||||
|
||||
if (hasEnvOverrides) {
|
||||
console.log('🔧 检测到环境变量覆盖,使用环境变量配置');
|
||||
return getConfigFromEnv(defaultConfig);
|
||||
}
|
||||
|
||||
console.log('🔧 最终配置:', defaultConfig);
|
||||
return defaultConfig;
|
||||
};
|
||||
|
||||
@@ -234,9 +376,12 @@ export const getCurrentPortConfig = () => {
|
||||
};
|
||||
|
||||
// 调试信息(仅在开发环境显示)
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// console.log('📦 API配置信息:', {
|
||||
// environment: getCurrentEnvironment(),
|
||||
// config: apiConfig
|
||||
// });
|
||||
// }
|
||||
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
|
||||
console.log('📦 API配置信息:', {
|
||||
environment: getCurrentEnvironment(),
|
||||
currentEnv: process.env.NODE_ENV,
|
||||
nextPublicEnv: process.env.NEXT_PUBLIC_API_ENV,
|
||||
port: getCurrentPort(),
|
||||
config: apiConfig
|
||||
});
|
||||
}
|
||||
+28
-2
@@ -29,6 +29,7 @@ import LoadingBarContainer from "~/components/ui/LoadingBar";
|
||||
import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
|
||||
// import { useState, useEffect } from "react";
|
||||
|
||||
|
||||
// 导入认证相关的服务器端功能(仅在服务器端使用)
|
||||
import {
|
||||
getUserSession,
|
||||
@@ -70,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 排除不需要登录验证的路径
|
||||
const publicPaths = ['/login', '/favicon.ico'];
|
||||
const publicPaths = ['/login', '/favicon.ico', '/callback'];
|
||||
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
||||
|
||||
// 获取用户会话(可能包含刷新后的token)
|
||||
@@ -108,6 +109,31 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
// 检查5178端口访问控制
|
||||
|
||||
// 由于应用直接运行在5178端口,我们需要从环境变量或运行时获取端口
|
||||
const currentPort = process.env.PORT || process.env.API_PORT_CONFIG;
|
||||
// console.log("currentPort-----------",currentPort)
|
||||
|
||||
// 获取运行时端口(从请求URL或环境变量)
|
||||
const runtimePort = url.port || currentPort;
|
||||
// console.log("runtimePort-----------",runtimePort)
|
||||
|
||||
const isPort51708 = currentPort === '5178' || runtimePort === '5178';
|
||||
|
||||
if (isPort51708 && !isPublicPath) {
|
||||
// 51708端口只允许访问交叉评查相关路径和首页
|
||||
const allowedPaths = ['/', '/cross-checking','/chat-with-llm'];
|
||||
const isAllowedPath = allowedPaths.some(path => pathname === path) ||
|
||||
pathname.startsWith('/cross-checking/') ||
|
||||
pathname.startsWith('/chat-with-llm/');
|
||||
|
||||
if (!isAllowedPath) {
|
||||
// console.log("5178端口访问受限,重定向到交叉评查页面");
|
||||
return redirect("/cross-checking");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果token被刷新了,需要在响应中设置更新后的cookie
|
||||
const responseHeaders: Record<string, string> = {};
|
||||
if (refreshedSession) {
|
||||
@@ -237,4 +263,4 @@ export function ErrorBoundary() {
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
+24
-12
@@ -45,6 +45,16 @@ export default function Index() {
|
||||
date: '',
|
||||
time: ''
|
||||
});
|
||||
|
||||
// 检查是否通过51708端口访问
|
||||
const [isPort51708, setIsPort51708] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// setIsPort51708(window.location.port === '51708');
|
||||
setIsPort51708(window.location.port === '5178');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 打印服务器端传递的用户角色
|
||||
useEffect(() => {
|
||||
@@ -142,18 +152,20 @@ export default function Index() {
|
||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||
|
||||
<div className="modules-container">
|
||||
{/* 合同管理模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
||||
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="合同管理"
|
||||
>
|
||||
<img src="/images/icon_hetong.png" alt="合同管理" className="w-12 h-12 mx-1" />
|
||||
<span className="module-name">合同管理</span>
|
||||
</div>
|
||||
{/* 合同管理模块 - 51708端口时隐藏 */}
|
||||
{!isPort51708 && (
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
||||
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="合同管理"
|
||||
>
|
||||
<img src="/images/icon_hetong.png" alt="合同管理" className="w-12 h-12 mx-1" />
|
||||
<span className="module-name">合同管理</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 案卷智能评查模块 */}
|
||||
<div
|
||||
|
||||
+11
-21
@@ -1,7 +1,7 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { sessionStorage, saveUserInfo } from "~/api/login/auth.server";
|
||||
import { createUserSession, saveUserInfo } from "~/api/login/auth.server";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
import { toastService } from "~/components/ui";
|
||||
|
||||
@@ -50,24 +50,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return redirect("/login?error=userinfo_error");
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
const session = await sessionStorage.getSession();
|
||||
session.set("isAuthenticated", true);
|
||||
session.set("accessToken", tokenResponse.access_token);
|
||||
session.set("refreshToken", tokenResponse.refresh_token);
|
||||
session.set("tokenIssuedAt", Date.now());
|
||||
session.set("tokenExpiresIn", tokenResponse.expires_in);
|
||||
session.set("userInfo", userInfo.data);
|
||||
|
||||
// TODO 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 暂定都是common
|
||||
// const userRole = userInfo.data.username === "admin" ? "developer" : "common";
|
||||
const userRole = "common";
|
||||
session.set("userRole", userRole);
|
||||
|
||||
// 获取重定向URL
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
|
||||
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
|
||||
const saveResult = await saveUserInfo(userInfo.data);
|
||||
@@ -97,9 +85,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
||||
console.log("前端JWT已生成");
|
||||
|
||||
// 将JWT存储在session中
|
||||
session.set("frontendJWT", frontendJWT);
|
||||
|
||||
// 更新userInfo以包含数据库ID和JWT信息
|
||||
const enhancedUserInfo = {
|
||||
...userInfo.data,
|
||||
@@ -107,12 +92,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
user_role: userRole,
|
||||
frontend_jwt: frontendJWT
|
||||
};
|
||||
session.set("userInfo", enhancedUserInfo);
|
||||
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
"Set-Cookie": cookie
|
||||
}
|
||||
// 使用统一的session创建函数
|
||||
return createUserSession({
|
||||
isAuthenticated: true,
|
||||
userRole: userRole as 'common' | 'developer',
|
||||
redirectTo,
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
tokenExpiresIn: tokenResponse.expires_in,
|
||||
userInfo: enhancedUserInfo,
|
||||
frontendJWT
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -135,6 +135,10 @@ export default function Home() {
|
||||
sessionStorage.removeItem('userRole');
|
||||
sessionStorage.removeItem('reviewType');
|
||||
sessionStorage.removeItem('previousReviewType');
|
||||
sessionStorage.removeItem('frontendJWT');
|
||||
sessionStorage.removeItem('userInfo');
|
||||
sessionStorage.removeItem('accessToken');
|
||||
sessionStorage.removeItem('isAuthenticated');
|
||||
// 可以根据需要清除其他会话数据
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
+178
-157
@@ -1,11 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } 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 { getUserSession, getSession, simpleRootLogin } from "~/api/login/auth.server";
|
||||
import styles from "~/styles/pages/login.css?url";
|
||||
import { toastService } from "~/components/ui";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: styles }
|
||||
@@ -44,112 +44,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
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();
|
||||
|
||||
if (intent === "test_user_login") {
|
||||
if (intent === "password_login") {
|
||||
// 获取重定向目标
|
||||
const session = await getSession(request);
|
||||
const redirectTo = session.get("redirectTo") || "/";
|
||||
|
||||
// 使用测试用户登录
|
||||
const testUserSub = "001"; // 测试用户的sub
|
||||
const userResult = await getUserBySub(testUserSub);
|
||||
// 调用 simpleRootLogin 方法进行登录
|
||||
const response = await simpleRootLogin(username || "", password || "", redirectTo);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
// 检查响应状态
|
||||
if (response.status === 302) {
|
||||
// 登录成功,直接返回重定向响应
|
||||
return response;
|
||||
} else {
|
||||
// 如果用户不存在,重定向到登录页面并显示错误
|
||||
return redirect(`/login?error=${encodeURIComponent("测试用户不存在")}`);
|
||||
// 登录失败,解析错误信息并重定向到登录页面
|
||||
const errorData = await response.json();
|
||||
const errorMsg = errorData.error || "登录失败";
|
||||
return redirect(`/login?error=${encodeURIComponent(errorMsg)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +73,9 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
export default function Login() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get("error");
|
||||
const [isFlipped, setIsFlipped] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
// 获取错误消息的友好描述
|
||||
const getErrorMessage = (error: string | null) => {
|
||||
@@ -175,6 +92,15 @@ export default function Login() {
|
||||
return "获取用户信息失败,请重新登录";
|
||||
case "callback_error":
|
||||
return "登录回调处理失败,请重新登录";
|
||||
case "用户名和密码不能为空":
|
||||
case "用户名和密码不能为空,请重新输入":
|
||||
return "用户名和密码不能为空,请重新输入";
|
||||
case "登录失败,请检查用户名和密码":
|
||||
case "用户名或密码错误,请重新输入":
|
||||
return "用户名或密码错误,请重新输入";
|
||||
case "登录请求失败,请稍后重试":
|
||||
case "网络连接失败,请稍后重试":
|
||||
return "网络连接失败,请稍后重试";
|
||||
default:
|
||||
return decodeURIComponent(error);
|
||||
}
|
||||
@@ -203,80 +129,175 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理管理员登录
|
||||
const handleAdminLogin = () => {
|
||||
setIsFlipped(true);
|
||||
};
|
||||
|
||||
// 处理返回OAuth登录
|
||||
const handleBackToOAuth = () => {
|
||||
setIsFlipped(false);
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
};
|
||||
|
||||
// 处理账号密码登录表单提交
|
||||
const handlePasswordLoginSubmit = (e: React.FormEvent) => {
|
||||
// 客户端验证
|
||||
if (!username.trim()) {
|
||||
e.preventDefault();
|
||||
toastService.error("请输入用户名");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
e.preventDefault();
|
||||
toastService.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证通过,让表单正常提交
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 检查OAuth配置是否完整
|
||||
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
|
||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
|
||||
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 className={`login-container ${isFlipped ? 'flipped' : ''}`}>
|
||||
<div className="login-card">
|
||||
{/* 正面 - OAuth登录 */}
|
||||
<div className="login-card-front">
|
||||
<div className="login-header">
|
||||
<h1 className="login-title">中国烟草AI合同及卷宗审核系统</h1>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleOAuthLogin}
|
||||
className="oauth-login-button"
|
||||
type="button"
|
||||
>
|
||||
<i className="ri-shield-user-line"></i>
|
||||
统一身份认证登录
|
||||
</button>
|
||||
<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="admin-login-link">
|
||||
<button
|
||||
onClick={handleAdminLogin}
|
||||
className="admin-login-text"
|
||||
type="button"
|
||||
>
|
||||
管理员登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-tips">
|
||||
<p>
|
||||
<i className="ri-information-line"></i>
|
||||
系统将跳转到统一身份认证平台进行登录
|
||||
</p>
|
||||
<div className="login-footer">
|
||||
<p>© 2025 中国烟草 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试用户登录区域 */}
|
||||
<div className="temp-login-section">
|
||||
<div className="section-divider">
|
||||
<span>或</span>
|
||||
{/* 背面 - 管理员登录 */}
|
||||
<div className="login-card-back">
|
||||
<div className="login-header">
|
||||
<h1 className="login-title">中国烟草AI合同及卷宗审核系统</h1>
|
||||
</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 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>
|
||||
)}
|
||||
|
||||
<Form method="post" className="admin-login-form" onSubmit={handlePasswordLoginSubmit}>
|
||||
<input type="hidden" name="intent" value="password_login" />
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="username" className="form-label">用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password" className="form-label">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="admin-login-button"
|
||||
>
|
||||
<i className="ri-login-box-line"></i>
|
||||
登录
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
<div className="back-to-oauth">
|
||||
<button
|
||||
onClick={handleBackToOAuth}
|
||||
className="back-button"
|
||||
type="button"
|
||||
>
|
||||
<i className="ri-arrow-left-line"></i>
|
||||
返回统一身份认证登录
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>© 2025 中国烟草 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>© 2025 中国烟草 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+344
-111
@@ -13,10 +13,44 @@
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
min-width: 320px;
|
||||
height: auto;
|
||||
min-height: 600px;
|
||||
max-height: 800px;
|
||||
padding: 2rem;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
transition: transform 0.8s;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.login-container.flipped .login-card {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.login-card-front,
|
||||
.login-card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
backface-visibility: hidden;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-card-back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
@@ -48,6 +82,11 @@
|
||||
|
||||
.login-form-container {
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* OAuth2.0 登录样式 */
|
||||
@@ -123,10 +162,133 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 管理员登录链接样式 */
|
||||
.admin-login-link {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-login-text {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #015c42;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-login-text:hover {
|
||||
color: #01704e;
|
||||
}
|
||||
|
||||
/* 管理员登录表单样式 */
|
||||
.admin-login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #015c42;
|
||||
box-shadow: 0 0 0 3px rgba(1, 92, 66, 0.1);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.admin-login-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: linear-gradient(135deg, #015c42 0%, #01704e 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-login-button:hover {
|
||||
background: linear-gradient(135deg, #01704e 0%, #015c42 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(1, 92, 66, 0.3);
|
||||
}
|
||||
|
||||
.admin-login-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.admin-login-button i {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 返回按钮样式 */
|
||||
.back-to-oauth {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background: none;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #666;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
border-color: #015c42;
|
||||
color: #015c42;
|
||||
background-color: rgba(1, 92, 66, 0.05);
|
||||
}
|
||||
|
||||
.back-button i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 错误消息样式 */
|
||||
.error-message-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: #fef2f2;
|
||||
@@ -134,24 +296,32 @@
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1.5rem;
|
||||
animation: fadeIn 0.3s ease;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #ef4444;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #dc2626;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
margin-top: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
@@ -165,10 +335,90 @@
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
/* 大屏幕 (1200px 及以上) */
|
||||
@media (min-width: 1200px) {
|
||||
.login-container {
|
||||
max-width: 600px;
|
||||
min-height: 700px;
|
||||
max-height: 900px;
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.oauth-login-button,
|
||||
.admin-login-button {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕 (768px - 1199px) */
|
||||
@media (min-width: 768px) and (max-width: 1199px) {
|
||||
.login-container {
|
||||
max-width: 540px;
|
||||
min-height: 650px;
|
||||
max-height: 850px;
|
||||
padding: 2.25rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.oauth-login-button,
|
||||
.admin-login-button {
|
||||
padding: 0.875rem 1.75rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕 (640px - 767px) */
|
||||
@media (min-width: 640px) and (max-width: 767px) {
|
||||
.login-container {
|
||||
max-width: 500px;
|
||||
min-height: 600px;
|
||||
max-height: 800px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动设备 (640px 及以下) */
|
||||
@media (max-width: 640px) {
|
||||
.login-container {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
min-height: 550px;
|
||||
max-height: 700px;
|
||||
}
|
||||
|
||||
.login-card-front,
|
||||
.login-card-back {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@@ -179,15 +429,73 @@
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.oauth-login-button {
|
||||
.oauth-login-button,
|
||||
.admin-login-button {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕 (480px 及以下) */
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
min-height: 500px;
|
||||
max-height: 650px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.login-card-front,
|
||||
.login-card-back {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.oauth-login-button,
|
||||
.admin-login-button {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 0.625rem 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色主题支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.login-container {
|
||||
.login-card-front,
|
||||
.login-card-back {
|
||||
background-color: #1f2937;
|
||||
color: #f9fafb;
|
||||
}
|
||||
@@ -207,114 +515,39 @@
|
||||
.login-footer {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
/* 临时管理员登录样式 */
|
||||
.temp-login-section {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.section-divider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: #e5e7eb;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-divider span {
|
||||
background-color: white;
|
||||
color: #9ca3af;
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.temp-login-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.temp-admin-login-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.temp-admin-login-button:hover {
|
||||
background: linear-gradient(135deg, #f97316 0%, #f59e0b 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.temp-admin-login-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.temp-admin-login-button i {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.temp-login-tips {
|
||||
text-align: center;
|
||||
color: #f59e0b;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.temp-login-tips p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.temp-login-tips i {
|
||||
font-size: 0.95rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* 暗色主题下的临时登录样式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.section-divider::before {
|
||||
background-color: #374151;
|
||||
|
||||
.form-label {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.section-divider span {
|
||||
background-color: #1f2937;
|
||||
color: #6b7280;
|
||||
.form-input {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #015c42;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
border-color: #4b5563;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
border-color: #015c42;
|
||||
color: #015c42;
|
||||
background-color: rgba(1, 92, 66, 0.1);
|
||||
}
|
||||
|
||||
.admin-login-text {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.admin-login-text:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user