添加管理员登陆,添加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 { tokenManager } from "./token-manager.server";
|
||||||
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
|
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
|
||||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
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重新生成信息
|
// 打印JWT重新生成信息
|
||||||
console.log("=== Token刷新时重新生成JWT ===");
|
console.log("=== Token刷新时重新生成JWT ===");
|
||||||
console.log("原始userInfo:", userInfo);
|
// console.log("原始userInfo:", userInfo);
|
||||||
console.log("重构的用户数据:", mockSavedUserData);
|
// console.log("重构的用户数据:", mockSavedUserData);
|
||||||
console.log("用户角色:", userRole);
|
// console.log("用户角色:", userRole);
|
||||||
console.log("新生成的JWT:", newJWT);
|
// console.log("新生成的JWT:", newJWT);
|
||||||
console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
|
// console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
|
||||||
|
|
||||||
// 更新session中的JWT
|
// 更新session中的JWT
|
||||||
if (!refreshedSession) {
|
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。
|
* 在用户成功登录后调用此函数来创建会话并设置 Cookie。
|
||||||
* 这个函数通常在以下场景中使用:
|
* 这个函数通常在以下场景中使用:
|
||||||
* - OAuth2.0 登录成功后
|
|
||||||
* - 临时管理员登录
|
* - 临时管理员登录
|
||||||
* - 其他认证方式成功后
|
* - 测试用户登录
|
||||||
|
* - 其他简单认证方式成功后
|
||||||
*
|
*
|
||||||
* 处理流程:
|
* 处理流程:
|
||||||
* 1. 创建新的会话对象
|
* 1. 创建新的会话对象
|
||||||
@@ -348,15 +415,15 @@ export async function getUserSession(request: Request) {
|
|||||||
* @param redirectTo - 登录成功后重定向的 URL,默认为首页
|
* @param redirectTo - 登录成功后重定向的 URL,默认为首页
|
||||||
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
|
* @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();
|
const session = await sessionStorage.getSession();
|
||||||
session.set("isAuthenticated", isAuthenticated);
|
session.set("isAuthenticated", isAuthenticated);
|
||||||
session.set("userRole", userRole);
|
session.set("userRole", userRole);
|
||||||
|
|
||||||
const cookie = await sessionStorage.commitSession(session);
|
const cookie = await sessionStorage.commitSession(session);
|
||||||
console.log("创建会话 - 设置Cookie:", !!cookie);
|
console.log("创建简化会话 - 设置Cookie:", !!cookie);
|
||||||
console.log("创建会话 - 用户角色:", userRole);
|
console.log("创建简化会话 - 用户角色:", userRole);
|
||||||
console.log("创建会话 - 重定向到:", redirectTo);
|
console.log("创建简化会话 - 重定向到:", redirectTo);
|
||||||
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302, // HTTP 重定向状态码
|
status: 302, // HTTP 重定向状态码
|
||||||
@@ -373,14 +440,15 @@ export async function createUserSession(isAuthenticated: boolean, userRole: User
|
|||||||
* 当用户主动登出或会话失效时调用此函数。
|
* 当用户主动登出或会话失效时调用此函数。
|
||||||
*
|
*
|
||||||
* 处理流程:
|
* 处理流程:
|
||||||
* 1. 获取当前用户的会话
|
* 1. 获取当前用户的会话和访问令牌
|
||||||
* 2. 销毁会话数据(清除所有存储的信息)
|
* 2. 调用 IDaaS 单点登出接口
|
||||||
* 3. 清除客户端的会话 Cookie
|
* 3. 销毁本地会话数据(清除所有存储的信息)
|
||||||
* 4. 重定向到登录页面
|
* 4. 清除客户端的会话 Cookie
|
||||||
|
* 5. 重定向到登录页面
|
||||||
*
|
*
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* - 这个函数只处理本地会话,不会调用 IDaaS 的单点登出
|
* - 这个函数会同时处理本地会话和 IDaaS 的单点登出
|
||||||
* - 如果需要全局登出,应该额外调用 IDaaS 的 SLO 接口
|
* - 即使 IDaaS 登出失败,也会清除本地会话
|
||||||
* - 销毁会话后,用户需要重新登录才能访问受保护的页面
|
* - 销毁会话后,用户需要重新登录才能访问受保护的页面
|
||||||
*
|
*
|
||||||
* @param request - Remix Request 对象,用于获取当前会话
|
* @param request - Remix Request 对象,用于获取当前会话
|
||||||
@@ -388,6 +456,21 @@ export async function createUserSession(isAuthenticated: boolean, userRole: User
|
|||||||
*/
|
*/
|
||||||
export async function logout(request: Request) {
|
export async function logout(request: Request) {
|
||||||
const session = await getSession(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, {
|
return new Response(null, {
|
||||||
status: 302, // HTTP 重定向状态码
|
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) {
|
export async function getUserBySub(sub: string) {
|
||||||
try {
|
try {
|
||||||
console.log(`查询用户: ${sub}`);
|
// console.log(`查询用户: ${sub}`);
|
||||||
|
|
||||||
const userResult = await postgrestGet<SsoUser[]>("sso_users", {
|
const userResult = await postgrestGet<SsoUser[]>("sso_users", {
|
||||||
filter: {
|
filter: {
|
||||||
@@ -595,49 +712,103 @@ export async function getUserBySub(sub: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建用户登录会话(支持用户信息)
|
* 账号密码登录接口
|
||||||
*
|
*
|
||||||
* @param isAuthenticated - 是否已认证
|
* @param username - 用户名
|
||||||
* @param userRole - 用户角色
|
* @param password - 密码
|
||||||
* @param redirectTo - 重定向URL
|
* @param redirectTo - 登录成功后重定向的URL
|
||||||
* @param userInfo - 可选的用户信息
|
* @returns HTTP重定向响应或错误响应
|
||||||
* @returns HTTP重定向响应
|
|
||||||
*/
|
*/
|
||||||
export async function createUserSessionWithInfo(
|
export async function simpleRootLogin(
|
||||||
isAuthenticated: boolean,
|
username: string,
|
||||||
userRole: UserRole,
|
password: string,
|
||||||
redirectTo: string,
|
redirectTo: string
|
||||||
userInfo?: Partial<SsoUser>
|
|
||||||
) {
|
) {
|
||||||
const session = await sessionStorage.getSession();
|
try {
|
||||||
session.set("isAuthenticated", isAuthenticated);
|
// 输入验证
|
||||||
session.set("userRole", userRole);
|
if (!username?.trim() || !password?.trim()) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
// 如果提供了用户信息,也保存到session中
|
success: false,
|
||||||
if (userInfo) {
|
error: "用户名和密码不能为空"
|
||||||
session.set("userInfo", {
|
}), {
|
||||||
sub: userInfo.sub,
|
status: 400,
|
||||||
user_id: userInfo.id,
|
headers: { "Content-Type": "application/json" }
|
||||||
username: userInfo.username,
|
});
|
||||||
nick_name: userInfo.nick_name,
|
}
|
||||||
email: userInfo.email,
|
|
||||||
ou_name: userInfo.ou_name,
|
|
||||||
is_leader: userInfo.is_leader,
|
|
||||||
user_role: userRole
|
// 调用登录接口
|
||||||
|
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 状态值字符串
|
* @returns 状态值字符串
|
||||||
*/
|
*/
|
||||||
generateState(): string {
|
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) +
|
const randomStr = Math.random().toString(36).substring(2, 15) +
|
||||||
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]
|
// const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP]
|
||||||
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
|
// 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 => {
|
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)) {
|
if (!visibleMenuIds.includes(item.id)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+170
-25
@@ -30,31 +30,82 @@ interface ApiConfig {
|
|||||||
// 端口特定配置映射
|
// 端口特定配置映射
|
||||||
// 根据不同端口提供不同的API配置
|
// 根据不同端口提供不同的API配置
|
||||||
const portConfigs: Record<string, Partial<ApiConfig>> = {
|
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': {
|
'51703': {
|
||||||
baseUrl: 'http://172.16.0.55:51703',
|
baseUrl: 'http://172.16.0.55:51703',
|
||||||
documentUrl: 'http://172.16.0.55:51703/docauditai/',
|
documentUrl: 'http://172.16.0.55:51703/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.55:51703/admin/documents'
|
uploadUrl: 'http://172.16.0.55:51703/admin/documents'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 潮州
|
||||||
'51704': {
|
'51704': {
|
||||||
baseUrl: 'http://172.16.0.55:51704',
|
baseUrl: 'http://172.16.0.55:51704',
|
||||||
documentUrl: 'http://172.16.0.55:51704/docauditai/',
|
documentUrl: 'http://172.16.0.55:51704/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.55:51704/admin/documents'
|
uploadUrl: 'http://172.16.0.55:51704/admin/documents'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 揭阳
|
||||||
'51705': {
|
'51705': {
|
||||||
baseUrl: 'http://172.16.0.55:51705',
|
baseUrl: 'http://172.16.0.55:51705',
|
||||||
documentUrl: 'http://172.16.0.55:51705/docauditai/',
|
documentUrl: 'http://172.16.0.55:51705/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.55:51705/admin/documents'
|
uploadUrl: 'http://172.16.0.55:51705/admin/documents'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 云浮
|
||||||
'51706': {
|
'51706': {
|
||||||
baseUrl: 'http://172.16.0.55:51706',
|
baseUrl: 'http://172.16.0.55:51706',
|
||||||
documentUrl: 'http://172.16.0.55:51706/docauditai/',
|
documentUrl: 'http://172.16.0.55:51706/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.55:51706/admin/documents'
|
uploadUrl: 'http://172.16.0.55:51706/admin/documents'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 梅州
|
||||||
'51707': {
|
'51707': {
|
||||||
baseUrl: 'http://172.16.0.55:51707',
|
baseUrl: 'http://172.16.0.55:51707',
|
||||||
documentUrl: 'http://172.16.0.55:51707/docauditai/',
|
documentUrl: 'http://172.16.0.55:51707/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.55:51707/admin/documents'
|
uploadUrl: 'http://172.16.0.55:51707/admin/documents'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 省局
|
||||||
'51708': {
|
'51708': {
|
||||||
baseUrl: 'http://172.16.0.55:51708',
|
baseUrl: 'http://172.16.0.55:51708',
|
||||||
documentUrl: 'http://172.16.0.55:51708/docauditai/',
|
documentUrl: 'http://172.16.0.55:51708/docauditai/',
|
||||||
@@ -86,14 +137,19 @@ const configs: Record<string, ApiConfig> = {
|
|||||||
|
|
||||||
// 测试环境
|
// 测试环境
|
||||||
testing: {
|
testing: {
|
||||||
baseUrl: 'http://nas.7bm.co:3000',
|
baseUrl: 'http://172.16.0.55:8008',
|
||||||
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
|
// baseUrl: 'http://172.16.0.81:3000',
|
||||||
uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload',
|
// 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: {
|
oauth: {
|
||||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', // 需要替换为实际的Client ID
|
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||||
clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret
|
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
|
||||||
redirectUri: 'http://nas.7bm.co:3000/callback', // 回调地址
|
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -101,8 +157,8 @@ const configs: Record<string, ApiConfig> = {
|
|||||||
// 生产环境
|
// 生产环境
|
||||||
production: {
|
production: {
|
||||||
// postgrest
|
// postgrest
|
||||||
baseUrl: 'http://172.16.0.55:8008',
|
// baseUrl: 'http://172.16.0.55:8008',
|
||||||
// baseUrl: 'http://10.79.97.17:8000',
|
baseUrl: 'http://10.79.97.17:8000',
|
||||||
// minio
|
// minio
|
||||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||||
// 文件上传
|
// 文件上传
|
||||||
@@ -133,8 +189,31 @@ const configs: Record<string, ApiConfig> = {
|
|||||||
|
|
||||||
// 获取当前环境,默认为development
|
// 获取当前环境,默认为development
|
||||||
const getCurrentEnvironment = (): string => {
|
const getCurrentEnvironment = (): string => {
|
||||||
// 优先使用环境变量,然后使用 NODE_ENV
|
// 在服务器端,优先使用PM2设置的环境变量
|
||||||
return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development';
|
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 => {
|
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) {
|
const nextPublicApiPortConfig = process.env.NEXT_PUBLIC_API_PORT_CONFIG;
|
||||||
return process.env.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) {
|
const args = process.argv;
|
||||||
return window.location.port;
|
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 env = getCurrentEnvironment();
|
||||||
const port = getCurrentPort();
|
const port = getCurrentPort();
|
||||||
|
|
||||||
|
console.log('🔧 配置调试信息:', {
|
||||||
|
environment: env,
|
||||||
|
port: port,
|
||||||
|
hasPortConfig: !!(port && portConfigs[port]),
|
||||||
|
portConfig: port ? portConfigs[port] : null
|
||||||
|
});
|
||||||
|
|
||||||
// 获取基础配置
|
// 获取基础配置
|
||||||
let defaultConfig = configs[env] || configs.development;
|
let defaultConfig = configs[env] || configs.development;
|
||||||
|
|
||||||
// 如果有端口特定配置,则合并配置
|
// 如果有端口特定配置,则合并配置
|
||||||
if (port && portConfigs[port]) {
|
if (port && portConfigs[port]) {
|
||||||
|
console.log(`🔧 使用端口特定配置: ${port}`, portConfigs[port]);
|
||||||
defaultConfig = {
|
defaultConfig = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...portConfigs[port],
|
...portConfigs[port],
|
||||||
// 保持oauth配置不变,只覆盖API相关配置
|
// 保持oauth配置不变,只覆盖API相关配置
|
||||||
oauth: defaultConfig.oauth
|
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);
|
return getConfigFromEnv(defaultConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('🔧 最终配置:', defaultConfig);
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,9 +376,12 @@ export const getCurrentPortConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 调试信息(仅在开发环境显示)
|
// 调试信息(仅在开发环境显示)
|
||||||
// if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
|
||||||
// console.log('📦 API配置信息:', {
|
console.log('📦 API配置信息:', {
|
||||||
// environment: getCurrentEnvironment(),
|
environment: getCurrentEnvironment(),
|
||||||
// config: apiConfig
|
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 RouteChangeLoader from "~/components/ui/RouteChangeLoader";
|
||||||
// import { useState, useEffect } from "react";
|
// import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
// 导入认证相关的服务器端功能(仅在服务器端使用)
|
// 导入认证相关的服务器端功能(仅在服务器端使用)
|
||||||
import {
|
import {
|
||||||
getUserSession,
|
getUserSession,
|
||||||
@@ -70,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
// 排除不需要登录验证的路径
|
// 排除不需要登录验证的路径
|
||||||
const publicPaths = ['/login', '/favicon.ico'];
|
const publicPaths = ['/login', '/favicon.ico', '/callback'];
|
||||||
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
||||||
|
|
||||||
// 获取用户会话(可能包含刷新后的token)
|
// 获取用户会话(可能包含刷新后的token)
|
||||||
@@ -108,6 +109,31 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
return redirect("/");
|
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
|
// 如果token被刷新了,需要在响应中设置更新后的cookie
|
||||||
const responseHeaders: Record<string, string> = {};
|
const responseHeaders: Record<string, string> = {};
|
||||||
if (refreshedSession) {
|
if (refreshedSession) {
|
||||||
@@ -237,4 +263,4 @@ export function ErrorBoundary() {
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+24
-12
@@ -45,6 +45,16 @@ export default function Index() {
|
|||||||
date: '',
|
date: '',
|
||||||
time: ''
|
time: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 检查是否通过51708端口访问
|
||||||
|
const [isPort51708, setIsPort51708] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// setIsPort51708(window.location.port === '51708');
|
||||||
|
setIsPort51708(window.location.port === '5178');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 打印服务器端传递的用户角色
|
// 打印服务器端传递的用户角色
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -142,18 +152,20 @@ export default function Index() {
|
|||||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||||
|
|
||||||
<div className="modules-container">
|
<div className="modules-container">
|
||||||
{/* 合同管理模块 */}
|
{/* 合同管理模块 - 51708端口时隐藏 */}
|
||||||
<div
|
{!isPort51708 && (
|
||||||
className="module-card"
|
<div
|
||||||
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
className="module-card"
|
||||||
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
||||||
role="button"
|
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
aria-label="合同管理"
|
tabIndex={0}
|
||||||
>
|
aria-label="合同管理"
|
||||||
<img src="/images/icon_hetong.png" alt="合同管理" className="w-12 h-12 mx-1" />
|
>
|
||||||
<span className="module-name">合同管理</span>
|
<img src="/images/icon_hetong.png" alt="合同管理" className="w-12 h-12 mx-1" />
|
||||||
</div>
|
<span className="module-name">合同管理</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 案卷智能评查模块 */}
|
{/* 案卷智能评查模块 */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
+11
-21
@@ -1,7 +1,7 @@
|
|||||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||||
import { OAuthClient } from "~/api/login/oauth-client";
|
import { OAuthClient } from "~/api/login/oauth-client";
|
||||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
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 { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||||
import { toastService } from "~/components/ui";
|
import { toastService } from "~/components/ui";
|
||||||
|
|
||||||
@@ -50,24 +50,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
return redirect("/login?error=userinfo_error");
|
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
|
// TODO 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 暂定都是common
|
||||||
// const userRole = userInfo.data.username === "admin" ? "developer" : "common";
|
// const userRole = userInfo.data.username === "admin" ? "developer" : "common";
|
||||||
const userRole = "common";
|
const userRole = "common";
|
||||||
session.set("userRole", userRole);
|
|
||||||
|
|
||||||
// 获取重定向URL
|
// 获取重定向URL
|
||||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||||
|
|
||||||
const cookie = await sessionStorage.commitSession(session);
|
|
||||||
|
|
||||||
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
|
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
|
||||||
const saveResult = await saveUserInfo(userInfo.data);
|
const saveResult = await saveUserInfo(userInfo.data);
|
||||||
@@ -97,9 +85,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
||||||
console.log("前端JWT已生成");
|
console.log("前端JWT已生成");
|
||||||
|
|
||||||
// 将JWT存储在session中
|
|
||||||
session.set("frontendJWT", frontendJWT);
|
|
||||||
|
|
||||||
// 更新userInfo以包含数据库ID和JWT信息
|
// 更新userInfo以包含数据库ID和JWT信息
|
||||||
const enhancedUserInfo = {
|
const enhancedUserInfo = {
|
||||||
...userInfo.data,
|
...userInfo.data,
|
||||||
@@ -107,12 +92,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
user_role: userRole,
|
user_role: userRole,
|
||||||
frontend_jwt: frontendJWT
|
frontend_jwt: frontendJWT
|
||||||
};
|
};
|
||||||
session.set("userInfo", enhancedUserInfo);
|
|
||||||
|
|
||||||
return redirect(redirectTo, {
|
// 使用统一的session创建函数
|
||||||
headers: {
|
return createUserSession({
|
||||||
"Set-Cookie": cookie
|
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) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ export default function Home() {
|
|||||||
sessionStorage.removeItem('userRole');
|
sessionStorage.removeItem('userRole');
|
||||||
sessionStorage.removeItem('reviewType');
|
sessionStorage.removeItem('reviewType');
|
||||||
sessionStorage.removeItem('previousReviewType');
|
sessionStorage.removeItem('previousReviewType');
|
||||||
|
sessionStorage.removeItem('frontendJWT');
|
||||||
|
sessionStorage.removeItem('userInfo');
|
||||||
|
sessionStorage.removeItem('accessToken');
|
||||||
|
sessionStorage.removeItem('isAuthenticated');
|
||||||
// 可以根据需要清除其他会话数据
|
// 可以根据需要清除其他会话数据
|
||||||
sessionStorage.clear();
|
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 { useSearchParams, Form } from "@remix-run/react";
|
||||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
||||||
import { OAuthClient } from "~/api/login/oauth-client";
|
import { OAuthClient } from "~/api/login/oauth-client";
|
||||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||||
import { getUserSession, getSession, sessionStorage, getUserBySub, addDefaultRole } from "~/api/login/auth.server";
|
import { getUserSession, getSession, simpleRootLogin } from "~/api/login/auth.server";
|
||||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
|
||||||
import styles from "~/styles/pages/login.css?url";
|
import styles from "~/styles/pages/login.css?url";
|
||||||
|
import { toastService } from "~/components/ui";
|
||||||
|
|
||||||
export const links = () => [
|
export const links = () => [
|
||||||
{ rel: "stylesheet", href: styles }
|
{ rel: "stylesheet", href: styles }
|
||||||
@@ -44,112 +44,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const intent = formData.get("intent");
|
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 session = await getSession(request);
|
||||||
const redirectTo = session.get("redirectTo") || "/";
|
const redirectTo = session.get("redirectTo") || "/";
|
||||||
|
|
||||||
// 使用测试用户登录
|
// 调用 simpleRootLogin 方法进行登录
|
||||||
const testUserSub = "001"; // 测试用户的sub
|
const response = await simpleRootLogin(username || "", password || "", redirectTo);
|
||||||
const userResult = await getUserBySub(testUserSub);
|
|
||||||
|
|
||||||
if (userResult.success && userResult.data) {
|
// 检查响应状态
|
||||||
const user = userResult.data;
|
if (response.status === 302) {
|
||||||
|
// 登录成功,直接返回重定向响应
|
||||||
// 确保用户有默认角色
|
return response;
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
} 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() {
|
export default function Login() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const error = searchParams.get("error");
|
const error = searchParams.get("error");
|
||||||
|
const [isFlipped, setIsFlipped] = useState(false);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
// 获取错误消息的友好描述
|
// 获取错误消息的友好描述
|
||||||
const getErrorMessage = (error: string | null) => {
|
const getErrorMessage = (error: string | null) => {
|
||||||
@@ -175,6 +92,15 @@ export default function Login() {
|
|||||||
return "获取用户信息失败,请重新登录";
|
return "获取用户信息失败,请重新登录";
|
||||||
case "callback_error":
|
case "callback_error":
|
||||||
return "登录回调处理失败,请重新登录";
|
return "登录回调处理失败,请重新登录";
|
||||||
|
case "用户名和密码不能为空":
|
||||||
|
case "用户名和密码不能为空,请重新输入":
|
||||||
|
return "用户名和密码不能为空,请重新输入";
|
||||||
|
case "登录失败,请检查用户名和密码":
|
||||||
|
case "用户名或密码错误,请重新输入":
|
||||||
|
return "用户名或密码错误,请重新输入";
|
||||||
|
case "登录请求失败,请稍后重试":
|
||||||
|
case "网络连接失败,请稍后重试":
|
||||||
|
return "网络连接失败,请稍后重试";
|
||||||
default:
|
default:
|
||||||
return decodeURIComponent(error);
|
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(() => {
|
useEffect(() => {
|
||||||
// 检查OAuth配置是否完整
|
// 检查OAuth配置是否完整
|
||||||
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
|
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
|
||||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
|
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<div className="login-container">
|
<div className={`login-container ${isFlipped ? 'flipped' : ''}`}>
|
||||||
<div className="login-header">
|
<div className="login-card">
|
||||||
<h1 className="login-title">中国烟草AI合同及卷宗审核系统</h1>
|
{/* 正面 - OAuth登录 */}
|
||||||
</div>
|
<div className="login-card-front">
|
||||||
|
<div className="login-header">
|
||||||
<div className="login-form-container">
|
<h1 className="login-title">中国烟草AI合同及卷宗审核系统</h1>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="login-form-container">
|
||||||
onClick={handleOAuthLogin}
|
<h2 className="login-subtitle">统一身份认证登录</h2>
|
||||||
className="oauth-login-button"
|
|
||||||
type="button"
|
{error && (
|
||||||
>
|
<div className="error-message-container">
|
||||||
<i className="ri-shield-user-line"></i>
|
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||||
统一身份认证登录
|
<div className="error-text">{getErrorMessage(error)}</div>
|
||||||
</button>
|
</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">
|
<div className="login-footer">
|
||||||
<p>
|
<p>© 2025 中国烟草 版权所有</p>
|
||||||
<i className="ri-information-line"></i>
|
|
||||||
系统将跳转到统一身份认证平台进行登录
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 测试用户登录区域 */}
|
{/* 背面 - 管理员登录 */}
|
||||||
<div className="temp-login-section">
|
<div className="login-card-back">
|
||||||
<div className="section-divider">
|
<div className="login-header">
|
||||||
<span>或</span>
|
<h1 className="login-title">中国烟草AI合同及卷宗审核系统</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form method="post" className="temp-login-form">
|
<div className="login-form-container">
|
||||||
<input type="hidden" name="intent" value="test_user_login" />
|
<h2 className="login-subtitle">管理员登录</h2>
|
||||||
<button
|
|
||||||
type="submit"
|
{error && (
|
||||||
className="temp-admin-login-button"
|
<div className="error-message-container">
|
||||||
>
|
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||||
<i className="ri-user-line"></i>
|
<div className="error-text">{getErrorMessage(error)}</div>
|
||||||
测试用户登录
|
</div>
|
||||||
</button>
|
)}
|
||||||
<div className="temp-login-tips">
|
|
||||||
<p>
|
<Form method="post" className="admin-login-form" onSubmit={handlePasswordLoginSubmit}>
|
||||||
<i className="ri-information-line"></i>
|
<input type="hidden" name="intent" value="password_login" />
|
||||||
使用测试用户(testuser1)登录,默认普通权限
|
|
||||||
</p>
|
<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>
|
</div>
|
||||||
</Form>
|
</div>
|
||||||
|
|
||||||
|
<div className="login-footer">
|
||||||
|
<p>© 2025 中国烟草 版权所有</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="login-footer">
|
|
||||||
<p>© 2025 中国烟草 版权所有</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+344
-111
@@ -13,10 +13,44 @@
|
|||||||
.login-container {
|
.login-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
|
min-width: 320px;
|
||||||
|
height: auto;
|
||||||
|
min-height: 600px;
|
||||||
|
max-height: 800px;
|
||||||
padding: 2rem;
|
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;
|
background-color: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
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 {
|
.login-header {
|
||||||
@@ -48,6 +82,11 @@
|
|||||||
|
|
||||||
.login-form-container {
|
.login-form-container {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* OAuth2.0 登录样式 */
|
/* OAuth2.0 登录样式 */
|
||||||
@@ -123,10 +162,133 @@
|
|||||||
color: #999;
|
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 {
|
.error-message-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background-color: #fef2f2;
|
background-color: #fef2f2;
|
||||||
@@ -134,24 +296,32 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
animation: fadeIn 0.3s ease;
|
animation: fadeIn 0.3s ease;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-icon {
|
.error-icon {
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
.error-text {
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-footer {
|
.login-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
margin-top: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-footer p {
|
.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) {
|
@media (max-width: 640px) {
|
||||||
.login-container {
|
.login-container {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
|
min-height: 550px;
|
||||||
|
max-height: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card-front,
|
||||||
|
.login-card-back {
|
||||||
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
@@ -179,15 +429,73 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oauth-login-button {
|
.oauth-login-button,
|
||||||
|
.admin-login-button {
|
||||||
padding: 0.75rem 1.25rem;
|
padding: 0.75rem 1.25rem;
|
||||||
font-size: 0.95rem;
|
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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.login-container {
|
.login-card-front,
|
||||||
|
.login-card-back {
|
||||||
background-color: #1f2937;
|
background-color: #1f2937;
|
||||||
color: #f9fafb;
|
color: #f9fafb;
|
||||||
}
|
}
|
||||||
@@ -207,114 +515,39 @@
|
|||||||
.login-footer {
|
.login-footer {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.form-label {
|
||||||
/* 临时管理员登录样式 */
|
color: #e5e7eb;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-divider span {
|
.form-input {
|
||||||
background-color: #1f2937;
|
background-color: #374151;
|
||||||
color: #6b7280;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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://<u>10.79.112.85</u>/oauth/authorize?response_type=code&scope=read&client_id=<u>54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO</u>&redirect_uri=<u>http%3a%2f%2f10.79.97.17%2f</u>&state=<u>10ff0be64971c07f893afc332877f68arS8FH2iyZni</u>
|
||||||
|
|
||||||
|
#### 🔗 请求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平台版本而有所差异,请以实际平台配置为准。
|
||||||
+79
-77
@@ -11,7 +11,8 @@ module.exports = {
|
|||||||
'-r', 'dotenv/config',
|
'-r', 'dotenv/config',
|
||||||
// './node_modules/.bin/remix-serve',
|
// './node_modules/.bin/remix-serve',
|
||||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||||
'./build/server/index.js'
|
'./build/server/index.js',
|
||||||
|
'--port', '51703'
|
||||||
],
|
],
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
@@ -20,12 +21,13 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: 51703,
|
PORT: 51703,
|
||||||
CLIENT_ID: 'main'
|
CLIENT_ID: 'main',
|
||||||
},
|
API_PORT_CONFIG: '51703',
|
||||||
env_testing: {
|
// 添加这些环境变量确保客户端能获取到
|
||||||
NODE_ENV: 'testing',
|
NEXT_PUBLIC_NODE_ENV: 'production',
|
||||||
PORT: 51703,
|
NEXT_PUBLIC_PORT: '51703',
|
||||||
CLIENT_ID: 'main'
|
NEXT_PUBLIC_CLIENT_ID: 'main',
|
||||||
|
NEXT_PUBLIC_API_PORT_CONFIG: '51703'
|
||||||
},
|
},
|
||||||
error_file: './logs/main-err.log',
|
error_file: './logs/main-err.log',
|
||||||
out_file: './logs/main-out.log',
|
out_file: './logs/main-out.log',
|
||||||
@@ -33,15 +35,16 @@ module.exports = {
|
|||||||
time: true
|
time: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// 客户端C - 反向代理服务 (端口: 51704)
|
// 客户端潮州 - 反向代理服务 (端口: 51704)
|
||||||
{
|
{
|
||||||
name: 'docreview-client-c',
|
name: 'docreview-client-chaozhou',
|
||||||
script: 'node',
|
script: 'node',
|
||||||
args: [
|
args: [
|
||||||
'-r', 'dotenv/config',
|
'-r', 'dotenv/config',
|
||||||
// './node_modules/.bin/remix-serve',
|
// './node_modules/.bin/remix-serve',
|
||||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||||
'./build/server/index.js'
|
'./build/server/index.js',
|
||||||
|
'--port', '51704'
|
||||||
],
|
],
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
@@ -50,29 +53,29 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: 51704,
|
PORT: 51704,
|
||||||
CLIENT_ID: 'client-c',
|
CLIENT_ID: 'chaozhou',
|
||||||
API_PORT_CONFIG: '51704'
|
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: {
|
error_file: './logs/chaozhou-err.log',
|
||||||
NODE_ENV: 'testing',
|
out_file: './logs/chaozhou-out.log',
|
||||||
PORT: 51704,
|
log_file: './logs/chaozhou-combined.log',
|
||||||
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',
|
|
||||||
time: true
|
time: true
|
||||||
},
|
},
|
||||||
// 客户端D - 独立服务 (端口: 51705)
|
// 客户端揭阳 - 独立服务 (端口: 51705)
|
||||||
{
|
{
|
||||||
name: 'docreview-client-d',
|
name: 'docreview-client-jieyang',
|
||||||
script: 'node',
|
script: 'node',
|
||||||
args: [
|
args: [
|
||||||
'-r', 'dotenv/config',
|
'-r', 'dotenv/config',
|
||||||
// './node_modules/.bin/remix-serve',
|
// './node_modules/.bin/remix-serve',
|
||||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||||
'./build/server/index.js'
|
'./build/server/index.js',
|
||||||
|
'--port', '51705'
|
||||||
],
|
],
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
@@ -81,29 +84,29 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: 51705,
|
PORT: 51705,
|
||||||
CLIENT_ID: 'client-d',
|
CLIENT_ID: 'jieyang',
|
||||||
API_PORT_CONFIG: '51705'
|
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: {
|
error_file: './logs/jieyang-err.log',
|
||||||
NODE_ENV: 'testing',
|
out_file: './logs/jieyang-out.log',
|
||||||
PORT: 51705,
|
log_file: './logs/jieyang-combined.log',
|
||||||
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',
|
|
||||||
time: true
|
time: true
|
||||||
},
|
},
|
||||||
// 客户端E - 独立服务 (端口: 51706)
|
// 客户端云浮 - 独立服务 (端口: 51706)
|
||||||
{
|
{
|
||||||
name: 'docreview-client-e',
|
name: 'docreview-client-yunfu',
|
||||||
script: 'node',
|
script: 'node',
|
||||||
args: [
|
args: [
|
||||||
'-r', 'dotenv/config',
|
'-r', 'dotenv/config',
|
||||||
// './node_modules/.bin/remix-serve',
|
// './node_modules/.bin/remix-serve',
|
||||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||||
'./build/server/index.js'
|
'./build/server/index.js',
|
||||||
|
'--port', '51706'
|
||||||
],
|
],
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
@@ -112,29 +115,29 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: 51706,
|
PORT: 51706,
|
||||||
CLIENT_ID: 'client-e',
|
CLIENT_ID: 'yunfu',
|
||||||
API_PORT_CONFIG: '51706'
|
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: {
|
error_file: './logs/yunfu-err.log',
|
||||||
NODE_ENV: 'testing',
|
out_file: './logs/yunfu-out.log',
|
||||||
PORT: 51706,
|
log_file: './logs/yunfu-combined.log',
|
||||||
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',
|
|
||||||
time: true
|
time: true
|
||||||
},
|
},
|
||||||
// 客户端F - 独立服务 (端口: 51707)
|
// 客户端梅州 - 独立服务 (端口: 51707)
|
||||||
{
|
{
|
||||||
name: 'docreview-client-f',
|
name: 'docreview-client-meizhou',
|
||||||
script: 'node',
|
script: 'node',
|
||||||
args: [
|
args: [
|
||||||
'-r', 'dotenv/config',
|
'-r', 'dotenv/config',
|
||||||
//'./node_modules/.bin/remix-serve',
|
//'./node_modules/.bin/remix-serve',
|
||||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||||
'./build/server/index.js'
|
'./build/server/index.js',
|
||||||
|
'--port', '51707'
|
||||||
],
|
],
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
@@ -143,29 +146,29 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: 51707,
|
PORT: 51707,
|
||||||
CLIENT_ID: 'client-f',
|
CLIENT_ID: 'meizhou',
|
||||||
API_PORT_CONFIG: '51707'
|
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: {
|
error_file: './logs/meizhou-err.log',
|
||||||
NODE_ENV: 'testing',
|
out_file: './logs/meizhou-out.log',
|
||||||
PORT: 51707,
|
log_file: './logs/meizhou-combined.log',
|
||||||
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',
|
|
||||||
time: true
|
time: true
|
||||||
},
|
},
|
||||||
// 客户端G - 独立服务 (端口: 51708)
|
// 客户端省局 - 独立服务 (端口: 51708)
|
||||||
{
|
{
|
||||||
name: 'docreview-client-g',
|
name: 'docreview-client-province',
|
||||||
script: 'node',
|
script: 'node',
|
||||||
args: [
|
args: [
|
||||||
'-r', 'dotenv/config',
|
'-r', 'dotenv/config',
|
||||||
//'./node_modules/.bin/remix-serve',
|
//'./node_modules/.bin/remix-serve',
|
||||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||||
'./build/server/index.js'
|
'./build/server/index.js',
|
||||||
|
'--port', '51708'
|
||||||
],
|
],
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
@@ -174,18 +177,17 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
PORT: 51708,
|
PORT: 51708,
|
||||||
CLIENT_ID: 'client-g',
|
CLIENT_ID: 'province',
|
||||||
API_PORT_CONFIG: '51708'
|
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: {
|
error_file: './logs/province-err.log',
|
||||||
NODE_ENV: 'testing',
|
out_file: './logs/province-out.log',
|
||||||
PORT: 51708,
|
log_file: './logs/province-combined.log',
|
||||||
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',
|
|
||||||
time: true
|
time: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
+23
-367
@@ -1,377 +1,33 @@
|
|||||||
# Ubuntu环境下的Nginx优化配置
|
# 基于 state 参数端口分发的 OAuth2 回调 Nginx 配置
|
||||||
# 支持多客户端代理和动态请求头传递
|
# 只保留回调分发相关配置,其他内容全部删除
|
||||||
|
|
||||||
# 上游服务器配置 - 指向开发服务器
|
# 1. 端口白名单映射(只允许指定端口)
|
||||||
upstream vite_dev_server {
|
map $arg_state $target_port {
|
||||||
server 172.16.0.34:5173;
|
default "";
|
||||||
# 连接池配置,提高性能
|
~^login(5173)_ 5173;
|
||||||
keepalive 32;
|
~^login(5174)_ 5174;
|
||||||
# 失败重试配置
|
~^login(5175)_ 5175;
|
||||||
# server 172.16.0.34:5173 backup; # 备用服务器(可选)
|
~^login(5176)_ 5176;
|
||||||
|
~^login(5177)_ 5177;
|
||||||
|
~^login(5178)_ 5178;
|
||||||
}
|
}
|
||||||
|
|
||||||
# 后端 API 服务器配置
|
# 2. 统一回调入口,根据 state 分发到对应端口
|
||||||
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)
|
|
||||||
server {
|
server {
|
||||||
listen 5174;
|
listen 80;
|
||||||
server_name localhost 127.0.0.1;
|
server_name 127.0.0.1;
|
||||||
|
|
||||||
# 设置客户端标识变量
|
location /callback {
|
||||||
set $client_id "client-a";
|
# 未匹配到允许端口直接返回 400
|
||||||
|
if ($target_port = "") {
|
||||||
# 访问日志 - 包含客户端信息
|
return 400 "Invalid or unsupported state/port";
|
||||||
access_log /var/log/nginx/client-a-access.log client_access;
|
}
|
||||||
error_log /var/log/nginx/client-a-error.log warn;
|
|
||||||
|
# 反向代理到本地对应端口的 /callback
|
||||||
# 主要代理配置
|
proxy_pass http://127.0.0.1:$target_port/callback$is_args$args;
|
||||||
location / {
|
|
||||||
# 反向代理到开发服务器
|
|
||||||
proxy_pass http://vite_dev_server;
|
|
||||||
|
|
||||||
# 基础代理头部
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+20
@@ -57,6 +57,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.25.1",
|
"esbuild": "^0.25.1",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
@@ -6667,6 +6668,25 @@
|
|||||||
"integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==",
|
"integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|||||||
+6
-3
@@ -5,12 +5,14 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"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",
|
"dev": "remix vite:dev",
|
||||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
"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": "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:multi": "npm run build:test:multi && pm2 start ecosystemDev.config.cjs --env testing",
|
||||||
"start:pm2:test": "npm run build:test && pm2 start ecosystem.config.cjs --env testing",
|
"start:pm2:production:multi": "npm run build:production:multi && pm2 start ecosystem.config.cjs --env production",
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.25.1",
|
"esbuild": "^0.25.1",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
|||||||
+26
-1
@@ -22,8 +22,33 @@ export default defineConfig({
|
|||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
],
|
],
|
||||||
define: {
|
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"),
|
"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: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
|
|||||||
Reference in New Issue
Block a user