From 33363aba7895a7286957b171f5dab450ae64a8b5 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Sun, 27 Jul 2025 20:01:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E7=99=BB=E9=99=86=EF=BC=8C=E6=B7=BB=E5=8A=A0nginx=E5=8F=8D?= =?UTF-8?q?=E5=90=91=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE=EF=BC=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/login/auth.server.ts | 289 ++++++++++++--- app/api/login/oauth-client.ts | 9 +- app/components/layout/Sidebar.tsx | 17 + app/config/api-config.ts | 195 ++++++++-- app/root.tsx | 30 +- app/routes/_index.tsx | 36 +- app/routes/callback.tsx | 32 +- app/routes/home.tsx | 4 + app/routes/login.tsx | 335 +++++++++-------- app/styles/pages/login.css | 455 +++++++++++++++++------ docs/OAuth2.0认证协议集成指南.md | 576 ++++++++++++++++++++++++++++++ ecosystem.config.cjs | 156 ++++---- ecosystemDev.config.cjs | 266 ++++++++++++++ nginx-ubuntu-optimized.conf | 390 ++------------------ package-lock.json | 20 ++ package.json | 9 +- vite.config.ts | 27 +- 17 files changed, 2010 insertions(+), 836 deletions(-) create mode 100644 docs/OAuth2.0认证协议集成指南.md create mode 100644 ecosystemDev.config.cjs diff --git a/app/api/login/auth.server.ts b/app/api/login/auth.server.ts index cd920c0..67823e4 100644 --- a/app/api/login/auth.server.ts +++ b/app/api/login/auth.server.ts @@ -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 + */ +async function callIDaaSLogout(accessToken: string, appId: string): Promise { + 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("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 +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, - }, - }); } \ No newline at end of file diff --git a/app/api/login/oauth-client.ts b/app/api/login/oauth-client.ts index f729c59..43d36e2 100644 --- a/app/api/login/oauth-client.ts +++ b/app/api/login/oauth-client.ts @@ -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; } } diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 393cd0e..b1e898d 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -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; diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 70d1d2f..fa5cbef 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -30,31 +30,82 @@ interface ApiConfig { // 端口特定配置映射 // 根据不同端口提供不同的API配置 const portConfigs: Record> = { + + // 测试主要服务实例 + '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 = { // 测试环境 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 = { // 生产环境 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 = { // 获取当前环境,默认为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 - // }); -// } \ No newline at end of file +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 + }); +} \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 2ada1a1..69f81fd 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -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 = {}; if (refreshedSession) { @@ -237,4 +263,4 @@ export function ErrorBoundary() { ); -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 70b1c2d..7251ea0 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -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() {

- 欢迎来到智慧法务平台 -

- {/* 合同管理模块 */} -
handleModuleClick('/contract-template/search', 'contract')} - onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)} - role="button" - tabIndex={0} - aria-label="合同管理" - > - 合同管理 - 合同管理 -
+ {/* 合同管理模块 - 51708端口时隐藏 */} + {!isPort51708 && ( +
handleModuleClick('/contract-template/search', 'contract')} + onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)} + role="button" + tabIndex={0} + aria-label="合同管理" + > + 合同管理 + 合同管理 +
+ )} {/* 案卷智能评查模块 */}
[ { 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 (
-
-
-

中国烟草AI合同及卷宗审核系统

-
- -
-

统一身份认证登录

- - {error && ( -
-
-
{getErrorMessage(error)}
-
- )} - -
-
-

请点击下方按钮进行统一身份认证登录

+
+
+ {/* 正面 - OAuth登录 */} +
+
+

中国烟草AI合同及卷宗审核系统

- +
+

统一身份认证登录

+ + {error && ( +
+
+
{getErrorMessage(error)}
+
+ )} + +
+
+

请点击下方按钮进行统一身份认证登录

+
+ + + +
+

+ + 系统将跳转到统一身份认证平台进行登录 +

+
+
+ + {/* 管理员登录链接 */} +
+ +
+
-
-

- - 系统将跳转到统一身份认证平台进行登录 -

+
+

© 2025 中国烟草 版权所有

- {/* 测试用户登录区域 */} -
-
- + {/* 背面 - 管理员登录 */} +
+
+

中国烟草AI合同及卷宗审核系统

-
- - -
-

- - 使用测试用户(testuser1)登录,默认普通权限 -

+
+

管理员登录

+ + {error && ( +
+
+
{getErrorMessage(error)}
+
+ )} + + + + +
+ + setUsername(e.target.value)} + className="form-input" + placeholder="请输入用户名" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="form-input" + placeholder="请输入密码" + required + /> +
+ + + + +
+
- +
+ +
+

© 2025 中国烟草 版权所有

+
- -
-

© 2025 中国烟草 版权所有

-
); diff --git a/app/styles/pages/login.css b/app/styles/pages/login.css index 8606ba9..6c6d81f 100644 --- a/app/styles/pages/login.css +++ b/app/styles/pages/login.css @@ -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; } -} \ No newline at end of file + + .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; + } +} + + \ No newline at end of file diff --git a/docs/OAuth2.0认证协议集成指南.md b/docs/OAuth2.0认证协议集成指南.md new file mode 100644 index 0000000..a77f8d5 --- /dev/null +++ b/docs/OAuth2.0认证协议集成指南.md @@ -0,0 +1,576 @@ +# OAuth2.0 认证协议集成开发指南 + +## 📋 目录 +- [1. 术语定义](#1-术语定义) +- [2. 业务场景说明](#2-业务场景说明) +- [3. 集成流程概览](#3-集成流程概览) +- [4. 详细集成步骤](#4-详细集成步骤) +- [5. API接口详解](#5-api接口详解) +- [6. 错误处理](#6-错误处理) +- [7. 注意事项](#7-注意事项) +- [8. 示例代码](#8-示例代码) + +## 1. 术语定义 + +### 🔍 核心概念 +| 术语 | 全称 | 说明 | +|------|------|------| +| **SP** | Service Provider | 业务系统,如OA系统、订单系统 | +| **IDaaS** | Identity as a Service | 提供统一身份服务的认证系统平台,即IDP | + +## 2. 业务场景说明 + +### 📱 应用场景 +业务系统作为SP,需要集成IDaaS的单点登录和单点登出功能。 + +### 🎯 核心目标 +- **单点登录**:用户通过IDaaS认证后,可以访问所有授权的应用 +- **单点登出**:用户在任意应用登出后,所有关联应用都会登出 +- **用户信息同步**:获取用户在IDaaS平台的身份信息 + +### 💡 实现方式 +1. **门户集成**:用户通过IDaaS门户选择应用进行登录 +2. **独立登录**:业务系统提供独立登录页面,调用IDaaS接口 +3. **API直接调用**:通过AK/SK方式直接调用IDaaS登录接口 + +## 3. 集成流程概览 + +```mermaid +sequenceDiagram + participant User as 用户 + participant SP as 业务系统(SP) + participant IDaaS as IDaaS平台 + + Note over User,IDaaS: 1. 配置OAuth2应用 + SP->>IDaaS: 在IDaaS平台创建OAuth2应用 + IDaaS-->>SP: 返回client_id和client_secret + + Note over User,IDaaS: 2. 用户登录流程 + User->>SP: 访问业务系统 + SP->>User: 重定向到IDaaS登录页 + User->>IDaaS: 在IDaaS完成登录 + IDaaS->>SP: 返回authorization code + SP->>IDaaS: 使用code获取access_token + IDaaS-->>SP: 返回access_token + SP->>IDaaS: 使用access_token获取用户信息 + IDaaS-->>SP: 返回用户详细信息 + SP-->>User: 完成登录,访问业务系统 + + Note over User,IDaaS: 3. 用户登出流程 + User->>SP: 请求登出 + SP->>IDaaS: 调用IDaaS登出接口 + IDaaS-->>SP: 登出成功 + SP-->>User: 重定向到登录页 +``` + +## 4. 详细集成步骤 + +### 4.1 配置OAuth2第三方应用 + +#### 📝 配置步骤 +1. 使用管理员登录IDaaS平台 +2. 创建新应用,选择标准协议 → OAuth2模式 +3. 配置应用基本信息 + +#### ⚙️ 关键配置项 +| 配置项 | 说明 | 示例 | +|--------|------|------| +| **Redirect URI** | 授权码模式下,接收IDaaS返回code的回调地址 | `http://oa.com/callback` | +| **Grant Type** | 授权类型,固定选择 | `authorization_code` | +| **Client ID** | 应用唯一标识 | `1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U` | +| **Client Secret** | 应用密钥 | `vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG` | + +### 4.2 对接IDaaS登录 + +#### 🚀 登录方式选择 + +##### 方式一:使用IDaaS统一登录页 +**适用场景**:不需要自定义登录页面样式的应用 + +**流程说明**: +1. 构建授权URL,引导用户跳转到IDaaS登录页 +2. 用户完成登录后,IDaaS回调业务系统 +3. 业务系统获取code,换取access_token +4. 使用access_token获取用户信息 + +## 5. API接口详解 + +### 5.1 获取授权码(Authorization Code) + +#### 📌 接口描述 +引导用户到IDaaS登录页面,获取授权码 + +http://10.79.112.85/oauth/authorize?response_type=code&scope=read&client_id=54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO&redirect_uri=http%3a%2f%2f10.79.97.17%2f&state=10ff0be64971c07f893afc332877f68arS8FH2iyZni + +#### 🔗 请求URL格式 +``` +http(s)://{IDaaS_server}/oauth/authorize?response_type=code&scope=read&client_id={client_id}&redirect_uri={redirect_uri}&state={state} +``` + +#### 📋 请求参数 +| 参数名 | 类型 | 必填 | 示例值 | 说明 | +|--------|------|------|--------|------| +| `response_type` | string | ✅ | `code` | 响应类型,固定为code | +| `scope` | string | ✅ | `read` | 授权范围,固定为read | +| `client_id` | string | ✅ | `1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U` | OAuth2应用的Client ID | +| `redirect_uri` | string | ✅ | `http%3A%2F%2Foa.com%2Fcallback` | 回调地址(需URL编码) | +| `state` | string | ✅ | `10ff0be64971c07f893afc332877f68arS8FH2iyZni_idp` | 状态值,建议包含`_idp`后缀 | + +#### 💡 完整示例 +``` +http://idaas.example.com/oauth/authorize?response_type=code&scope=read&client_id=1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U&redirect_uri=http%3A%2F%2Foa.com%2Fcallback&state=10ff0be64971c07f893afc332877f68arS8FH2iyZni_idp +``` + +### 5.2 获取访问令牌(Access Token) + +#### 📌 接口描述 +使用授权码换取访问令牌 + +#### 🔗 请求信息 +- **URL**: `http(s)://{IDaaS_server}/oauth/token` +- **方法**: `POST` +- **Content-Type**: `application/x-www-form-urlencoded` + +#### 📋 请求参数 +| 参数名 | 类型 | 必填 | 示例值 | 说明 | +|--------|------|------|--------|------| +| `grant_type` | string | ✅ | `authorization_code` | 授权类型,固定值 | +| `code` | string | ✅ | `WgWQe6` | 从回调中获取的授权码 | +| `client_id` | string | ✅ | `1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U` | OAuth2应用的Client ID | +| `client_secret` | string | ✅ | `vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG` | OAuth2应用的Client Secret | +| `redirect_uri` | string | ✅ | `http%3A%2F%2Foa.com%2Fcallback` | 回调地址(需URL编码) | + +#### 📤 cURL示例 +```bash +curl -X POST 'http://idaas.example.com/oauth/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'grant_type=authorization_code&code=dIKvfA&client_id=1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U&client_secret=vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG&redirect_uri=http%3A%2F%2Foa.com%2Fcallback' +``` + +#### ✅ 成功响应 +```json +{ + "access_token": "eyJhbGciO...", + "token_type": "bearer", + "refresh_token": "eyJhbGciOiJIUzI1...", + "expires_in": 7199, + "scope": "read", + "jti": "17147278-7f3e-45f2-be6f-8105c4334a30" +} +``` + +### 5.3 获取用户信息 + +#### 📌 接口描述 +使用访问令牌获取用户详细信息 + +#### 🔗 请求信息 +- **URL**: `https://{IDaaS_server}/api/bff/v1.2/oauth2/userinfo` +- **方法**: `GET` +- **认证**: Bearer Token + +#### 📋 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `access_token` | string | ✅ | 访问令牌(可作为URL参数或Header) | + +#### 📤 请求示例 +```bash +# 方式1: URL参数 +GET https://idaas.example.com/api/bff/v1.2/oauth2/userinfo?access_token=eyJhbGc1NiIs... + +# 方式2: Authorization Header +curl -H "Authorization: Bearer eyJhbGc1NiIs..." \ + https://idaas.example.com/api/bff/v1.2/oauth2/userinfo +``` + +#### ✅ 成功响应 +```json +{ + "success": true, + "code": "200", + "message": null, + "requestId": "149DA248-8F49-4820-B87A-5EA36D932354", + "data": { + "sub": "823071756087671783", + "ou_id": "2079225187122667069", + "nickname": "测试用户", + "phone_number": "11136618971", + "ou_name": "测试组织IDAAS", + "email": "test@test.com", + "username": "test" + } +} +``` + +#### 📊 响应字段说明 +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `sub` | string | 用户唯一标识 | +| `ou_id` | string | 组织ID | +| `nickname` | string | 用户昵称 | +| `phone_number` | string | 手机号码 | +| `ou_name` | string | 组织名称 | +| `email` | string | 邮箱地址 | +| `username` | string | 用户名 | + +### 5.4 单点登出(SLO) + +#### 📌 接口描述 +实现全局统一登出功能 + +#### 🔗 请求信息 +- **URL**: `http(s)://{IDaaS_server}/public/sp/slo/{appId}` +- **方法**: `GET` 或 `POST`(推荐POST) + +#### 📋 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `appId` | string | ✅ | 应用ID(路径参数) | +| `redirect_url` | string | ❌ | 登出成功后的重定向URL(需URL编码) | +| `access_token` | string | ❌ | 用户的访问令牌 | + +#### 📤 请求示例 +```bash +# GET请求 +http://idaas.example.com/public/sp/slo/idaasoauth2?access_token=xxxxxxx&redirect_url=https%3A%2F%2Fwww.example.com%2F + +# POST请求(推荐) +curl -X POST 'http://idaas.example.com/public/sp/slo/idaasoauth2' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'access_token=xxxxxxx&redirect_url=https%3A%2F%2Fwww.example.com%2F' +``` + +## 6. 错误处理 + +### ❌ 常见错误响应 + +#### Token相关错误 +```json +// 客户端认证失败 +{ + "error": "invalid_client", + "error_description": "Bad client credentials" +} + +// 授权码无效 +{ + "error": "invalid_grant", + "error_description": "Invalid authorization code: dIKvfA" +} + +// 授权码过期 +{ + "error": "invalid_grant", + "error_description": "authorization code expired: WgWQe6" +} +``` + +#### HTTP状态码说明 +| 状态码 | 错误类型 | 说明 | +|--------|----------|------| +| `401` | Unauthorized | 未授权的访问 | +| `403` | Forbidden | 权限不足 | +| `404` | ResourceNotFound | 访问的资源不存在 | +| `415` | UnsupportedMediaType | 不支持的媒体类型 | +| `500` | InternalError | 服务器内部错误 | + +## 7. 注意事项 + +### ⚠️ 重要提醒 + +#### 多端访问处理 +当企业内网同时有PC端Web应用和移动端H5应用时,需要根据`remote-user`请求头字段进行判断: + +- **`remote-user`为NULL**: 从企业内网登录 → 使用原始地址 +- **`remote-user`不为NULL**: 从企业外网登录 → 使用代理地址 + +#### URL地址转换规则 +``` +原始地址: http://xx.YY.zzz.AA +代理地址: https://xx-YY-zzz-AA-kkkkkkkkkkkk.ztna-dingtalk.com +``` + +#### 移动端适配 +可以通过UserAgent等信息进行设备类型判断,实现不同终端的差异化跳转。 + +### 🔐 安全建议 + +1. **HTTPS传输**: 生产环境务必使用HTTPS协议 +2. **State参数**: 使用随机且不可预测的state值防止CSRF攻击 +3. **Token保护**: 妥善保存client_secret和access_token +4. **回调验证**: 验证回调请求的来源和参数完整性 +5. **Token过期**: 及时处理token过期和刷新逻辑 + +## 8. 示例代码 + +### 🐍 Python集成示例 + +```python +import requests +import urllib.parse +from typing import Dict, Optional + +class IDaaSClient: + def __init__(self, server_url: str, client_id: str, client_secret: str, redirect_uri: str): + self.server_url = server_url.rstrip('/') + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_authorize_url(self, state: str) -> str: + """生成授权URL""" + params = { + 'response_type': 'code', + 'scope': 'read', + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'state': state + } + + query_string = urllib.parse.urlencode(params) + return f"{self.server_url}/oauth/authorize?{query_string}" + + def get_access_token(self, code: str) -> Optional[Dict]: + """使用授权码获取访问令牌""" + url = f"{self.server_url}/oauth/token" + data = { + 'grant_type': 'authorization_code', + 'code': code, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri + } + + try: + response = requests.post(url, data=data) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"获取token失败: {e}") + return None + + def get_user_info(self, access_token: str) -> Optional[Dict]: + """获取用户信息""" + url = f"{self.server_url}/api/bff/v1.2/oauth2/userinfo" + headers = {'Authorization': f'Bearer {access_token}'} + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"获取用户信息失败: {e}") + return None + + def logout(self, app_id: str, access_token: str, redirect_url: str) -> bool: + """单点登出""" + url = f"{self.server_url}/public/sp/slo/{app_id}" + data = { + 'access_token': access_token, + 'redirect_url': redirect_url + } + + try: + response = requests.post(url, data=data) + return response.status_code == 200 + except requests.RequestException as e: + print(f"登出失败: {e}") + return False + +# 使用示例 +if __name__ == "__main__": + # 初始化客户端 + client = IDaaSClient( + server_url="http://idaas.example.com", + client_id="your_client_id", + client_secret="your_client_secret", + redirect_uri="http://your-app.com/callback" + ) + + # 1. 生成登录URL(重定向用户到此URL) + state = "random_state_value_with_idp" + login_url = client.get_authorize_url(state) + print(f"登录URL: {login_url}") + + # 2. 处理回调(从query参数获取code) + code = "received_code_from_callback" + token_response = client.get_access_token(code) + + if token_response: + access_token = token_response['access_token'] + print(f"Access Token: {access_token}") + + # 3. 获取用户信息 + user_info = client.get_user_info(access_token) + if user_info and user_info['success']: + user_data = user_info['data'] + print(f"用户信息: {user_data}") + + # 4. 登出 + logout_success = client.logout("your_app_id", access_token, "http://your-app.com/login") + print(f"登出结果: {'成功' if logout_success else '失败'}") +``` + +### 🌐 JavaScript集成示例 + +```javascript +class IDaaSClient { + constructor(serverUrl, clientId, clientSecret, redirectUri) { + this.serverUrl = serverUrl.replace(/\/$/, ''); + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUri = redirectUri; + } + + // 生成授权URL + getAuthorizeUrl(state) { + const params = new URLSearchParams({ + response_type: 'code', + scope: 'read', + client_id: this.clientId, + redirect_uri: this.redirectUri, + state: state + }); + + return `${this.serverUrl}/oauth/authorize?${params.toString()}`; + } + + // 获取访问令牌 + async getAccessToken(code) { + const url = `${this.serverUrl}/oauth/token`; + const data = new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_id: this.clientId, + client_secret: this.clientSecret, + redirect_uri: this.redirectUri + }); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: data + }); + + return await response.json(); + } catch (error) { + console.error('获取token失败:', error); + return null; + } + } + + // 获取用户信息 + async getUserInfo(accessToken) { + const url = `${this.serverUrl}/api/bff/v1.2/oauth2/userinfo`; + + try { + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + return await response.json(); + } catch (error) { + console.error('获取用户信息失败:', error); + return null; + } + } + + // 单点登出 + async logout(appId, accessToken, redirectUrl) { + const url = `${this.serverUrl}/public/sp/slo/${appId}`; + const data = new URLSearchParams({ + access_token: accessToken, + redirect_url: redirectUrl + }); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: data + }); + + return response.ok; + } catch (error) { + console.error('登出失败:', error); + return false; + } + } +} + +// 使用示例 +const client = new IDaaSClient( + 'http://idaas.example.com', + 'your_client_id', + 'your_client_secret', + 'http://your-app.com/callback' +); + +// 处理登录流程 +async function handleLogin() { + // 1. 重定向到IDaaS登录页 + const state = 'random_state_value_with_idp'; + const loginUrl = client.getAuthorizeUrl(state); + window.location.href = loginUrl; +} + +// 处理回调 +async function handleCallback() { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const state = urlParams.get('state'); + + if (code) { + // 2. 获取访问令牌 + const tokenResponse = await client.getAccessToken(code); + + if (tokenResponse && tokenResponse.access_token) { + const accessToken = tokenResponse.access_token; + + // 3. 获取用户信息 + const userInfo = await client.getUserInfo(accessToken); + + if (userInfo && userInfo.success) { + console.log('用户信息:', userInfo.data); + // 保存用户信息到localStorage或状态管理 + localStorage.setItem('access_token', accessToken); + localStorage.setItem('user_info', JSON.stringify(userInfo.data)); + } + } + } +} + +// 处理登出 +async function handleLogout() { + const accessToken = localStorage.getItem('access_token'); + + if (accessToken) { + const success = await client.logout('your_app_id', accessToken, window.location.origin + '/login'); + + if (success) { + localStorage.removeItem('access_token'); + localStorage.removeItem('user_info'); + window.location.href = '/login'; + } + } +} +``` + +--- + +## 📞 技术支持 + +如需更多技术支持,请参考: +- IDaaS平台管理后台 +- 相关API文档 +- 集成Demo项目 + +**注意**: 本文档基于OAuth2.0标准协议,具体实现可能因IDaaS平台版本而有所差异,请以实际平台配置为准。 \ No newline at end of file diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index fcd95b4..b6a6258 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -11,7 +11,8 @@ module.exports = { '-r', 'dotenv/config', // './node_modules/.bin/remix-serve', './node_modules/@remix-run/serve/dist/cli.js', - './build/server/index.js' + './build/server/index.js', + '--port', '51703' ], instances: 1, autorestart: true, @@ -20,12 +21,13 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 51703, - CLIENT_ID: 'main' - }, - env_testing: { - NODE_ENV: 'testing', - PORT: 51703, - CLIENT_ID: 'main' + CLIENT_ID: 'main', + API_PORT_CONFIG: '51703', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'production', + NEXT_PUBLIC_PORT: '51703', + NEXT_PUBLIC_CLIENT_ID: 'main', + NEXT_PUBLIC_API_PORT_CONFIG: '51703' }, error_file: './logs/main-err.log', out_file: './logs/main-out.log', @@ -33,15 +35,16 @@ module.exports = { time: true }, - // 客户端C - 反向代理服务 (端口: 51704) + // 客户端潮州 - 反向代理服务 (端口: 51704) { - name: 'docreview-client-c', + name: 'docreview-client-chaozhou', script: 'node', args: [ '-r', 'dotenv/config', // './node_modules/.bin/remix-serve', './node_modules/@remix-run/serve/dist/cli.js', - './build/server/index.js' + './build/server/index.js', + '--port', '51704' ], instances: 1, autorestart: true, @@ -50,29 +53,29 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 51704, - CLIENT_ID: 'client-c', - API_PORT_CONFIG: '51704' + CLIENT_ID: 'chaozhou', + API_PORT_CONFIG: '51704', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'production', + NEXT_PUBLIC_PORT: '51704', + NEXT_PUBLIC_CLIENT_ID: 'chaozhou', + NEXT_PUBLIC_API_PORT_CONFIG: '51704' }, - env_testing: { - NODE_ENV: 'testing', - PORT: 51704, - CLIENT_ID: 'client-c', - API_PORT_CONFIG: '51704' - }, - error_file: './logs/client-c-err.log', - out_file: './logs/client-c-out.log', - log_file: './logs/client-c-combined.log', + error_file: './logs/chaozhou-err.log', + out_file: './logs/chaozhou-out.log', + log_file: './logs/chaozhou-combined.log', time: true }, - // 客户端D - 独立服务 (端口: 51705) + // 客户端揭阳 - 独立服务 (端口: 51705) { - name: 'docreview-client-d', + name: 'docreview-client-jieyang', script: 'node', args: [ '-r', 'dotenv/config', // './node_modules/.bin/remix-serve', './node_modules/@remix-run/serve/dist/cli.js', - './build/server/index.js' + './build/server/index.js', + '--port', '51705' ], instances: 1, autorestart: true, @@ -81,29 +84,29 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 51705, - CLIENT_ID: 'client-d', - API_PORT_CONFIG: '51705' + CLIENT_ID: 'jieyang', + API_PORT_CONFIG: '51705', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'production', + NEXT_PUBLIC_PORT: '51705', + NEXT_PUBLIC_CLIENT_ID: 'jieyang', + NEXT_PUBLIC_API_PORT_CONFIG: '51705' }, - env_testing: { - NODE_ENV: 'testing', - PORT: 51705, - CLIENT_ID: 'client-d', - API_PORT_CONFIG: '51705' - }, - error_file: './logs/client-d-err.log', - out_file: './logs/client-d-out.log', - log_file: './logs/client-d-combined.log', + error_file: './logs/jieyang-err.log', + out_file: './logs/jieyang-out.log', + log_file: './logs/jieyang-combined.log', time: true }, - // 客户端E - 独立服务 (端口: 51706) + // 客户端云浮 - 独立服务 (端口: 51706) { - name: 'docreview-client-e', + name: 'docreview-client-yunfu', script: 'node', args: [ '-r', 'dotenv/config', // './node_modules/.bin/remix-serve', './node_modules/@remix-run/serve/dist/cli.js', - './build/server/index.js' + './build/server/index.js', + '--port', '51706' ], instances: 1, autorestart: true, @@ -112,29 +115,29 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 51706, - CLIENT_ID: 'client-e', - API_PORT_CONFIG: '51706' + CLIENT_ID: 'yunfu', + API_PORT_CONFIG: '51706', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'production', + NEXT_PUBLIC_PORT: '51706', + NEXT_PUBLIC_CLIENT_ID: 'yunfu', + NEXT_PUBLIC_API_PORT_CONFIG: '51706' }, - env_testing: { - NODE_ENV: 'testing', - PORT: 51706, - CLIENT_ID: 'client-e', - API_PORT_CONFIG: '51706' - }, - error_file: './logs/client-e-err.log', - out_file: './logs/client-e-out.log', - log_file: './logs/client-e-combined.log', + error_file: './logs/yunfu-err.log', + out_file: './logs/yunfu-out.log', + log_file: './logs/yunfu-combined.log', time: true }, - // 客户端F - 独立服务 (端口: 51707) + // 客户端梅州 - 独立服务 (端口: 51707) { - name: 'docreview-client-f', + name: 'docreview-client-meizhou', script: 'node', args: [ '-r', 'dotenv/config', //'./node_modules/.bin/remix-serve', './node_modules/@remix-run/serve/dist/cli.js', - './build/server/index.js' + './build/server/index.js', + '--port', '51707' ], instances: 1, autorestart: true, @@ -143,29 +146,29 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 51707, - CLIENT_ID: 'client-f', - API_PORT_CONFIG: '51707' + CLIENT_ID: 'meizhou', + API_PORT_CONFIG: '51707', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'production', + NEXT_PUBLIC_PORT: '51707', + NEXT_PUBLIC_CLIENT_ID: 'meizhou', + NEXT_PUBLIC_API_PORT_CONFIG: '51707' }, - env_testing: { - NODE_ENV: 'testing', - PORT: 51707, - CLIENT_ID: 'client-f', - API_PORT_CONFIG: '51707' - }, - error_file: './logs/client-f-err.log', - out_file: './logs/client-f-out.log', - log_file: './logs/client-f-combined.log', + error_file: './logs/meizhou-err.log', + out_file: './logs/meizhou-out.log', + log_file: './logs/meizhou-combined.log', time: true }, - // 客户端G - 独立服务 (端口: 51708) + // 客户端省局 - 独立服务 (端口: 51708) { - name: 'docreview-client-g', + name: 'docreview-client-province', script: 'node', args: [ '-r', 'dotenv/config', //'./node_modules/.bin/remix-serve', './node_modules/@remix-run/serve/dist/cli.js', - './build/server/index.js' + './build/server/index.js', + '--port', '51708' ], instances: 1, autorestart: true, @@ -174,18 +177,17 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 51708, - CLIENT_ID: 'client-g', - API_PORT_CONFIG: '51708' + CLIENT_ID: 'province', + API_PORT_CONFIG: '51708', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'production', + NEXT_PUBLIC_PORT: '51708', + NEXT_PUBLIC_CLIENT_ID: 'province', + NEXT_PUBLIC_API_PORT_CONFIG: '51708' }, - env_testing: { - NODE_ENV: 'testing', - PORT: 51708, - CLIENT_ID: 'client-g', - API_PORT_CONFIG: '51708' - }, - error_file: './logs/client-g-err.log', - out_file: './logs/client-g-out.log', - log_file: './logs/client-g-combined.log', + error_file: './logs/province-err.log', + out_file: './logs/province-out.log', + log_file: './logs/province-combined.log', time: true } ], diff --git a/ecosystemDev.config.cjs b/ecosystemDev.config.cjs new file mode 100644 index 0000000..d87624a --- /dev/null +++ b/ecosystemDev.config.cjs @@ -0,0 +1,266 @@ +// ecosystem.config.cjs - CommonJS 版本 +// 多客户端部署配置:支持3个不同地区客户端通过不同端口访问 + +module.exports = { + apps: [ + // 主服务 - 生产环境 (端口: 5173) + { + name: 'docreview-main', + script: 'node', + args: [ + '-r', 'dotenv/config', + // './node_modules/.bin/remix-serve', + './node_modules/@remix-run/serve/dist/cli.js', + './build/server/index.js', + '--port', '5173' + ], + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'testing', + PORT: 5173, + CLIENT_ID: 'main', + API_PORT_CONFIG: '5173', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5173', + NEXT_PUBLIC_CLIENT_ID: 'main', + NEXT_PUBLIC_API_PORT_CONFIG: '5173', + // REMIX_DEV_ORIGIN: 'http://localhost:5173' + }, + env_testing: { + NODE_ENV: 'testing', + PORT: 5173, + CLIENT_ID: 'main', + API_PORT_CONFIG: '5173', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5173', + NEXT_PUBLIC_CLIENT_ID: 'main', + NEXT_PUBLIC_API_PORT_CONFIG: '5173' + }, + error_file: './logs/main-err.log', + out_file: './logs/main-out.log', + log_file: './logs/main-combined.log', + time: true + }, + + // 客户端潮州 - 反向代理服务 (端口: 5174) + { + name: 'docreview-client-chaozhou', + script: 'node', + args: [ + '-r', 'dotenv/config', + // './node_modules/.bin/remix-serve', + './node_modules/@remix-run/serve/dist/cli.js', + './build/server/index.js', + '--port', '5174' + ], + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'testing', + PORT: 5174, + CLIENT_ID: 'chaozhou', + API_PORT_CONFIG: '5174', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5174', + NEXT_PUBLIC_CLIENT_ID: 'chaozhou', + NEXT_PUBLIC_API_PORT_CONFIG: '5174', + // REMIX_DEV_ORIGIN: 'http://localhost:5174' + }, + env_testing: { + NODE_ENV: 'testing', + PORT: 5174, + CLIENT_ID: 'chaozhou', + API_PORT_CONFIG: '5174', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5174', + NEXT_PUBLIC_CLIENT_ID: 'chaozhou', + NEXT_PUBLIC_API_PORT_CONFIG: '5174' + }, + error_file: './logs/chaozhou-err.log', + out_file: './logs/chaozhou-out.log', + log_file: './logs/chaozhou-combined.log', + time: true + }, + // 客户端揭阳 - 独立服务 (端口: 5175) + { + name: 'docreview-client-jieyang', + script: 'node', + args: [ + '-r', 'dotenv/config', + // './node_modules/.bin/remix-serve', + './node_modules/@remix-run/serve/dist/cli.js', + './build/server/index.js', + '--port', '5175' + ], + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'testing', + PORT: 5175, + CLIENT_ID: 'jieyang', + API_PORT_CONFIG: '5175', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5175', + NEXT_PUBLIC_CLIENT_ID: 'jieyang', + NEXT_PUBLIC_API_PORT_CONFIG: '5175', + // REMIX_DEV_ORIGIN: 'http://localhost:5175' + }, + env_testing: { + NODE_ENV: 'testing', + PORT: 5175, + CLIENT_ID: 'jieyang', + API_PORT_CONFIG: '5175', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5175', + NEXT_PUBLIC_CLIENT_ID: 'jieyang', + NEXT_PUBLIC_API_PORT_CONFIG: '5175' + }, + error_file: './logs/jieyang-err.log', + out_file: './logs/jieyang-out.log', + log_file: './logs/jieyang-combined.log', + time: true + }, + // 客户端云浮 - 独立服务 (端口: 5176) + { + name: 'docreview-client-yunfu', + script: 'node', + args: [ + '-r', 'dotenv/config', + // './node_modules/.bin/remix-serve', + './node_modules/@remix-run/serve/dist/cli.js', + './build/server/index.js', + '--port', '5176' + ], + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'testing', + PORT: 5176, + CLIENT_ID: 'yunfu', + API_PORT_CONFIG: '5176', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5176', + NEXT_PUBLIC_CLIENT_ID: 'yunfu', + NEXT_PUBLIC_API_PORT_CONFIG: '5176', + // REMIX_DEV_ORIGIN: 'http://localhost:5176' + }, + env_testing: { + NODE_ENV: 'testing', + PORT: 5176, + CLIENT_ID: 'yunfu', + API_PORT_CONFIG: '5176', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5176', + NEXT_PUBLIC_CLIENT_ID: 'yunfu', + NEXT_PUBLIC_API_PORT_CONFIG: '5176' + }, + error_file: './logs/yunfu-err.log', + out_file: './logs/yunfu-out.log', + log_file: './logs/yunfu-combined.log', + time: true + }, + // 客户端梅州 - 独立服务 (端口: 5177) + { + name: 'docreview-client-meizhou', + script: 'node', + args: [ + '-r', 'dotenv/config', + //'./node_modules/.bin/remix-serve', + './node_modules/@remix-run/serve/dist/cli.js', + './build/server/index.js', + '--port', '5177' + ], + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'testing', + PORT: 5177, + CLIENT_ID: 'meizhou', + API_PORT_CONFIG: '5177', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5177', + NEXT_PUBLIC_CLIENT_ID: 'meizhou', + NEXT_PUBLIC_API_PORT_CONFIG: '5177', + // REMIX_DEV_ORIGIN: 'http://localhost:5177' + }, + env_testing: { + NODE_ENV: 'testing', + PORT: 5177, + CLIENT_ID: 'meizhou', + API_PORT_CONFIG: '5177', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5177', + NEXT_PUBLIC_CLIENT_ID: 'meizhou', + NEXT_PUBLIC_API_PORT_CONFIG: '5177' + }, + error_file: './logs/meizhou-err.log', + out_file: './logs/meizhou-out.log', + log_file: './logs/meizhou-combined.log', + time: true + }, + // 客户端省局 - 独立服务 (端口: 5178) + { + name: 'docreview-client-province', + script: 'node', + args: [ + '-r', 'dotenv/config', + //'./node_modules/.bin/remix-serve', + './node_modules/@remix-run/serve/dist/cli.js', + './build/server/index.js', + '--port', '5178' + ], + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'testing', + PORT: 5178, + CLIENT_ID: 'province', + API_PORT_CONFIG: '5178', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5178', + NEXT_PUBLIC_CLIENT_ID: 'province', + NEXT_PUBLIC_API_PORT_CONFIG: '5178', + // REMIX_DEV_ORIGIN: 'http://localhost:5178' + }, + env_testing: { + NODE_ENV: 'testing', + PORT: 5178, + CLIENT_ID: 'province', + API_PORT_CONFIG: '5178', + // 添加这些环境变量确保客户端能获取到 + NEXT_PUBLIC_NODE_ENV: 'testing', + NEXT_PUBLIC_PORT: '5178', + NEXT_PUBLIC_CLIENT_ID: 'province', + NEXT_PUBLIC_API_PORT_CONFIG: '5178' + }, + error_file: './logs/province-err.log', + out_file: './logs/province-out.log', + log_file: './logs/province-combined.log', + time: true + } + ] +}; \ No newline at end of file diff --git a/nginx-ubuntu-optimized.conf b/nginx-ubuntu-optimized.conf index 3a8e21c..56337aa 100644 --- a/nginx-ubuntu-optimized.conf +++ b/nginx-ubuntu-optimized.conf @@ -1,377 +1,33 @@ -# Ubuntu环境下的Nginx优化配置 -# 支持多客户端代理和动态请求头传递 +# 基于 state 参数端口分发的 OAuth2 回调 Nginx 配置 +# 只保留回调分发相关配置,其他内容全部删除 -# 上游服务器配置 - 指向开发服务器 -upstream vite_dev_server { - server 172.16.0.34:5173; - # 连接池配置,提高性能 - keepalive 32; - # 失败重试配置 - # server 172.16.0.34:5173 backup; # 备用服务器(可选) +# 1. 端口白名单映射(只允许指定端口) +map $arg_state $target_port { + default ""; + ~^login(5173)_ 5173; + ~^login(5174)_ 5174; + ~^login(5175)_ 5175; + ~^login(5176)_ 5176; + ~^login(5177)_ 5177; + ~^login(5178)_ 5178; } -# 后端 API 服务器配置 -upstream api_client_a { - server 172.16.0.34:5174; - keepalive 32; -} - -upstream api_client_b { - server 172.16.0.34:5175; - keepalive 32; -} - -upstream api_client_c { - server 172.16.0.34:5176; - keepalive 32; -} - -upstream api_client_d { - server 172.16.0.34:5177; - keepalive 32; -} - -# 日志格式定义 - 包含客户端标识 -log_format client_access '$remote_addr - $remote_user [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent" ' - 'client_id="$client_id" original_port="$server_port"'; - -# 客户端A配置 (端口5174) +# 2. 统一回调入口,根据 state 分发到对应端口 server { - listen 5174; - server_name localhost 127.0.0.1; - - # 设置客户端标识变量 - set $client_id "client-a"; - - # 访问日志 - 包含客户端信息 - access_log /var/log/nginx/client-a-access.log client_access; - error_log /var/log/nginx/client-a-error.log warn; - - # 主要代理配置 - location / { - # 反向代理到开发服务器 - proxy_pass http://vite_dev_server; - - # 基础代理头部 + listen 80; + server_name 127.0.0.1; + + location /callback { + # 未匹配到允许端口直接返回 400 + if ($target_port = "") { + return 400 "Invalid or unsupported state/port"; + } + + # 反向代理到本地对应端口的 /callback + proxy_pass http://127.0.0.1:$target_port/callback$is_args$args; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - - # 客户端特定头部 - 用于应用识别客户端 - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - proxy_set_header X-Forwarded-Port $server_port; - - # 开发环境特殊配置 - 支持Vite热重载 - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - - # 连接超时配置 - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - - # 禁用缓冲以支持实时更新 - proxy_buffering off; - proxy_cache off; - proxy_request_buffering off; - - # 处理大文件上传 - client_max_body_size 100M; - - # 开发环境安全头部(相对宽松) - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - - # 开发环境CORS支持 - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always; - add_header Access-Control-Allow-Credentials true always; - } - - # 健康检查端点 - location /health { - access_log off; - return 200 "Client A (Port 5174) - OK\n"; - add_header Content-Type text/plain; - add_header X-Client-ID $client_id; - } - - # API代理特殊处理 - 修改为代理到对应的后端API服务器 - location /api/ { - proxy_pass http://api_client_a; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - - # API请求超时配置 - proxy_connect_timeout 10s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - # 全局错误页面配置 - error_page 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - - # 处理OPTIONS预检请求 - if ($request_method = 'OPTIONS') { - return 204; - } -} - -# 客户端B配置 (端口5175) -server { - listen 5175; - server_name localhost 127.0.0.1; - - set $client_id "client-b"; - - access_log /var/log/nginx/client-b-access.log client_access; - error_log /var/log/nginx/client-b-error.log warn; - - location / { - proxy_pass http://vite_dev_server; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - proxy_set_header X-Forwarded-Port $server_port; - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - - proxy_buffering off; - proxy_cache off; - proxy_request_buffering off; - - client_max_body_size 100M; - - # 开发环境安全头部(相对宽松) - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - - # 开发环境CORS支持 - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always; - add_header Access-Control-Allow-Credentials true always; - } - - location /health { - access_log off; - return 200 "Client B (Port 5175) - OK\n"; - add_header Content-Type text/plain; - add_header X-Client-ID $client_id; - } - - # API代理特殊处理 - 修改为代理到对应的后端API服务器 - location /api/ { - proxy_pass http://api_client_b; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - - # API请求超时配置 - proxy_connect_timeout 10s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - # 全局错误页面配置 - error_page 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - - # 处理OPTIONS预检请求 - if ($request_method = 'OPTIONS') { - return 204; - } -} - -# 客户端C配置 (端口5176) -server { - listen 5176; - server_name localhost 127.0.0.1; - - set $client_id "client-c"; - - access_log /var/log/nginx/client-c-access.log client_access; - error_log /var/log/nginx/client-c-error.log warn; - - location / { - proxy_pass http://vite_dev_server; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - proxy_set_header X-Forwarded-Port $server_port; - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - - proxy_buffering off; - proxy_cache off; - proxy_request_buffering off; - - client_max_body_size 100M; - - # 开发环境安全头部(相对宽松) - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - - # 开发环境CORS支持 - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always; - add_header Access-Control-Allow-Credentials true always; - } - - location /health { - access_log off; - return 200 "Client C (Port 5176) - OK\n"; - add_header Content-Type text/plain; - add_header X-Client-ID $client_id; - } - - # API代理特殊处理 - 修改为代理到对应的后端API服务器 - location /api/ { - proxy_pass http://api_client_c; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - - # API请求超时配置 - proxy_connect_timeout 10s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - # 全局错误页面配置 - error_page 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - - # 处理OPTIONS预检请求 - if ($request_method = 'OPTIONS') { - return 204; - } -} - -# 客户端D配置 (端口5177) - 预留扩展 -server { - listen 5177; - server_name localhost 127.0.0.1; - - set $client_id "client-d"; - - access_log /var/log/nginx/client-d-access.log client_access; - error_log /var/log/nginx/client-d-error.log warn; - - location / { - proxy_pass http://vite_dev_server; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - proxy_set_header X-Forwarded-Port $server_port; - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - - proxy_buffering off; - proxy_cache off; - proxy_request_buffering off; - - client_max_body_size 100M; - - # 开发环境安全头部(相对宽松) - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - - # 开发环境CORS支持 - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always; - add_header Access-Control-Allow-Credentials true always; - } - - location /health { - access_log off; - return 200 "Client D (Port 5177) - OK\n"; - add_header Content-Type text/plain; - add_header X-Client-ID $client_id; - } - - # API代理特殊处理 - 修改为代理到对应的后端API服务器 - location /api/ { - proxy_pass http://api_client_d; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Client-ID $client_id; - proxy_set_header X-Original-Port $server_port; - - # API请求超时配置 - proxy_connect_timeout 10s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - # 全局错误页面配置 - error_page 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - - # 处理OPTIONS预检请求 - if ($request_method = 'OPTIONS') { - return 204; } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f7fce17..c8765b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "autoprefixer": "^10.4.21", + "cross-env": "^7.0.3", "esbuild": "^0.25.1", "eslint": "^8.38.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -6667,6 +6668,25 @@ "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==", "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/package.json b/package.json index dfca4d5..ebcdea4 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,14 @@ "type": "module", "scripts": { "build": "remix vite:build", - "build:test": "NODE_ENV=testing remix vite:build", + "build:production:multi": "cross-env NODE_ENV=production remix vite:build", + "build:test:multi": "cross-env NODE_ENV=testing remix vite:build", + "build:dev": "cross-env NODE_ENV=development remix vite:build", "dev": "remix vite:dev", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "start": "node -r dotenv/config ./node_modules/.bin/remix-serve ./build/server/index.js", - "start:pm2": "npm run build && pm2 start ecosystem.config.cjs", - "start:pm2:test": "npm run build:test && pm2 start ecosystem.config.cjs --env testing", + "start:pm2:multi": "npm run build:test:multi && pm2 start ecosystemDev.config.cjs --env testing", + "start:pm2:production:multi": "npm run build:production:multi && pm2 start ecosystem.config.cjs --env production", "typecheck": "tsc" }, "dependencies": { @@ -65,6 +67,7 @@ "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "autoprefixer": "^10.4.21", + "cross-env": "^7.0.3", "esbuild": "^0.25.1", "eslint": "^8.38.0", "eslint-import-resolver-typescript": "^3.6.1", diff --git a/vite.config.ts b/vite.config.ts index a11829a..75d6bfe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,8 +22,33 @@ export default defineConfig({ tsconfigPaths(), ], define: { - // 在构建时为客户端代码提供 process.env.NODE_ENV 变量 + // 在构建时为客户端代码提供环境变量 + + // NODE_ENV: 当前运行环境,影响API配置选择 + // - development: 开发环境,使用本地API服务器 + // - testing: 测试环境,使用测试API服务器 + // - production: 生产环境,使用生产API服务器 "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), + + // 注意:移除端口相关的硬编码,改为运行时动态获取 + // PORT和API_PORT_CONFIG将在运行时通过环境变量或启动参数获取 + + // CLIENT_ID: 客户端标识,用于区分不同地区的客户端(可选) + // - main: 主服务 + // - chaozhou: 潮州客户端 + // - jieyang: 揭阳客户端 + // - yunfu: 云浮客户端 + // - meizhou: 梅州客户端 + // - province: 省局客户端 + "process.env.CLIENT_ID": JSON.stringify(process.env.CLIENT_ID || "main"), + + // NEXT_PUBLIC_前缀的环境变量,确保客户端能获取到 + "process.env.NEXT_PUBLIC_NODE_ENV": JSON.stringify(process.env.NEXT_PUBLIC_NODE_ENV || process.env.NODE_ENV || "development"), + "process.env.NEXT_PUBLIC_CLIENT_ID": JSON.stringify(process.env.NEXT_PUBLIC_CLIENT_ID || process.env.CLIENT_ID || "main"), + + // 注意:移除了 NEXT_PUBLIC_API_BASE_URL 和 NEXT_PUBLIC_API_ENV + // 这些变量会覆盖多客户端配置逻辑,导致配置冲突 + // 如需特殊配置,请直接修改 api-config.ts 中的配置 }, server: { host: '0.0.0.0',