1. 添加新的正式环境的secret配置信息。

2. 动态回调地址,如果是钉钉应用则用对应的回调地址。
3. 高频错误评查点改成显示出错次数。
4. 添加开关的通用组件,评查点列表方便修改状态。
This commit is contained in:
2026-01-19 16:22:21 +08:00
parent e332d05e5d
commit 1fca1a2e2e
8 changed files with 270 additions and 18 deletions
+25 -1
View File
@@ -43,6 +43,7 @@ interface UserInfoResponse {
export class OAuthClient {
private config: OAuthConfig;
private runtimeRedirectUri?: string; // 运行时回调地址(用于钉钉/内网动态切换)
constructor(config: OAuthConfig) {
this.config = {
@@ -64,6 +65,28 @@ export class OAuthClient {
}
}
/**
* 设置运行时回调地址(用于钉钉/内网动态切换)
* @param redirectUri 回调地址
*/
setRedirectUri(redirectUri: string): void {
this.runtimeRedirectUri = redirectUri;
console.log('🔧 [OAuthClient] 运行时回调地址已设置:', redirectUri);
}
/**
* 获取当前使用的回调地址
* 优先使用运行时设置的回调地址,否则使用配置中的默认地址
* @returns 回调地址
*/
private getRedirectUri(): string {
const uri = this.runtimeRedirectUri || this.config.redirectUri || '';
if (this.runtimeRedirectUri) {
console.log('🔧 [OAuthClient] 使用运行时回调地址:', uri);
}
return uri;
}
/**
* 生成授权URL
* @param state 状态值,建议包含随机字符串和_idp后缀
@@ -96,12 +119,13 @@ export class OAuthClient {
*/
async getAccessToken(code: string): Promise<TokenResponse | null> {
const url = `${this.config.serverUrl}/oauth/token`;
const redirectUri = this.getRedirectUri(); // 使用动态回调地址
const data = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret || '', // 提供默认值避免类型错误
redirect_uri: this.config.redirectUri
redirect_uri: redirectUri
});
console.log('🔧 请求Token URL:', url);
+49
View File
@@ -0,0 +1,49 @@
/* 开关组件 - 用于切换启用/禁用状态 */
interface SwitchProps {
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
loading?: boolean;
className?: string;
id?: string;
}
export function Switch({
checked,
onChange,
disabled = false,
loading = false,
className = '',
id
}: SwitchProps) {
const handleClick = () => {
if (!disabled && !loading && onChange) {
onChange(!checked);
}
};
const isDisabled = disabled || loading;
return (
<button
type="button"
id={id}
className={`
switch
${checked ? 'switch-checked' : 'switch-unchecked'}
${isDisabled ? 'switch-disabled' : ''}
${loading ? 'switch-loading' : ''}
${className}
`}
onClick={handleClick}
disabled={isDisabled}
aria-checked={checked}
role="switch"
>
<span className={`switch-handle ${checked ? 'switch-handle-checked' : 'switch-handle-unchecked'}`}>
{loading && <span className="switch-loading-icon"></span>}
</span>
</button>
);
}
+18 -10
View File
@@ -23,8 +23,10 @@ interface ApiConfig {
clientId?: string;
// OAuth2应用Client Secret
clientSecret?: string;
// 回调地址
// 回调地址(内网Web
redirectUri?: string;
// 钉钉Web回调地址(互联网地址)
dingtalkRedirectUri?: string;
// 应用ID(用于登出)
appId?: string;
};
@@ -39,7 +41,7 @@ interface ApiConfig {
// 端口特定配置映射
// 根据不同端口提供不同的API配置
const portConfigs: Record<string, Partial<ApiConfig>> = {
export const portConfigs: Record<string, Partial<ApiConfig>> = {
// 主要
// 梅州
@@ -65,7 +67,9 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
appUrl: 'http://10.79.97.17:51703',
oauth: {
redirectUri: 'http://10.79.97.17:51703/callback'
redirectUri: 'http://10.79.97.17:51703/callback',
// 钉钉Web回调地址(互联网地址)- 需要根据实际部署修改
dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51703 || 'https://10-79-97-1751703-b2oaixksdrrisox0t3.ztna-dingtalk.com/callback'
}
},
@@ -84,7 +88,8 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
collaboraUrl: 'http://10.79.97.17:9980',
appUrl: 'http://10.79.97.17:51704',
oauth: {
redirectUri: 'http://10.79.97.17:51704/callback'
redirectUri: 'http://10.79.97.17:51704/callback',
dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51704 || 'https://10-79-97-1751704-xxxxxxxxx.ztna-dingtalk.com/callback'
}
},
@@ -96,7 +101,8 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
collaboraUrl: 'http://10.79.97.17:9980',
appUrl: 'http://10.79.97.17:51705',
oauth: {
redirectUri: 'http://10.79.97.17:51705/callback'
redirectUri: 'http://10.79.97.17:51705/callback',
dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51705 || 'https://10-79-97-1751705-xxxxxxxxx.ztna-dingtalk.com/callback'
}
},
@@ -108,7 +114,8 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
collaboraUrl: 'http://10.79.97.17:9980',
appUrl: 'http://10.79.97.17:51706',
oauth: {
redirectUri: 'http://10.79.97.17:51706/callback'
redirectUri: 'http://10.79.97.17:51706/callback',
dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51706 || 'https://10-79-97-1751706-xxxxxxxxx.ztna-dingtalk.com/callback'
}
},
@@ -129,7 +136,8 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
appUrl: 'http://10.79.97.17:51707',
oauth: {
redirectUri: 'http://10.79.97.17:51707/callback'
redirectUri: 'http://10.79.97.17:51707/callback',
dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51707 || 'https://10-79-97-1751707-xxxxxxxxx.ztna-dingtalk.com/callback'
}
},
@@ -202,9 +210,9 @@ const configs: Record<string, ApiConfig> = {
collaboraUrl: 'http://10.79.97.17:9980',
appUrl: 'http://10.79.97.17:51703',
oauth: {
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址(测试)
// serverUrl: 'http://10.79.97.252', // IDaaS服务器地址(生产)
clientId: '224266374b56ee6254ed3d339014b033kaZy92exUmy',
// serverUrl: 'http://10.79.112.85', // IDaaS服务器地址(测试)
serverUrl: 'http://10.79.97.252', // IDaaS服务器地址(生产)
// ⚠️ 安全警告:clientSecret 不应该硬编码在代码中
// 请在生产环境使用环境变量 OAUTH_CLIENT_SECRET
clientSecret: 'placeholder', // 占位符,实际值从环境变量获取
+32 -1
View File
@@ -5,7 +5,7 @@
* Remix 会自动排除 .server.ts 文件不打包到客户端
*/
import { OAUTH_CONFIG } from './api-config';
import { OAUTH_CONFIG, portConfigs } from './api-config';
// 用于控制日志输出(避免重复日志)
let hasLoggedSecret = false;
@@ -67,3 +67,34 @@ export function getServerOAuthConfigRuntime() {
};
}
/**
* 获取端口特定的OAuth配置(包含钉钉回调地址)
* @param port 端口号
* @returns OAuth配置(包含内网和钉钉回调地址)
*/
export function getPortOAuthConfig(port: string) {
const secret = getOAuthClientSecret();
const portConfig = portConfigs[port];
if (!portConfig?.oauth) {
console.warn(`⚠️ [oauth-secret.server] 端口 ${port} 没有特定OAuth配置,使用默认配置`);
return {
serverUrl: OAUTH_CONFIG.serverUrl!,
clientId: OAUTH_CONFIG.clientId!,
redirectUri: OAUTH_CONFIG.redirectUri!,
appId: OAUTH_CONFIG.appId!,
clientSecret: secret,
dingtalkRedirectUri: undefined
};
}
return {
serverUrl: OAUTH_CONFIG.serverUrl!,
clientId: OAUTH_CONFIG.clientId!,
redirectUri: portConfig.oauth.redirectUri || OAUTH_CONFIG.redirectUri!,
appId: OAUTH_CONFIG.appId!,
clientSecret: secret,
dingtalkRedirectUri: portConfig.oauth.dingtalkRedirectUri
};
}
+29 -2
View File
@@ -3,7 +3,7 @@ import { useEffect } from "react";
import { useSearchParams } from "@remix-run/react";
import { createUserSession, sessionStorage } from "~/api/login/auth.server";
import { OAuthClient } from "~/api/login/oauth-client";
import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server";
import { getServerOAuthConfigRuntime, getPortOAuthConfig } from "~/config/oauth-secret.server";
import { loginWithOAuth, type LoginRequest } from "~/api/login/login-client";
import { isMobileDevice, MOBILE_CHAT_PATH } from "~/utils/mobile-detect.server";
@@ -106,9 +106,36 @@ export async function loader({ request }: LoaderFunctionArgs) {
console.log("✅ OAuth2.0回调参数验证通过");
// 🔑 判断是否从钉钉登录:检查 remote-user header
const remoteUser = request.headers.get("remote-user");
const isDingTalkLogin = remoteUser !== null;
console.log("🔧 [Callback] remote-user 检测:", {
remoteUser: remoteUser,
isDingTalkLogin: isDingTalkLogin,
loginType: isDingTalkLogin ? '钉钉Web登录' : '内网Web登录'
});
// 声明在 try 外部,以便在 catch 中访问
let tokenResponse = null;
const oauthClient = new OAuthClient(getServerOAuthConfigRuntime());
// 获取端口特定的OAuth配置(包含钉钉回调地址)
const portOAuthConfig = getPortOAuthConfig(port);
const oauthClient = new OAuthClient(portOAuthConfig);
// 🔑 根据登录类型设置回调地址
if (isDingTalkLogin) {
// 钉钉登录:使用钉钉回调地址
if (portOAuthConfig.dingtalkRedirectUri) {
oauthClient.setRedirectUri(portOAuthConfig.dingtalkRedirectUri);
console.log("🔧 [Callback] 使用钉钉回调地址:", portOAuthConfig.dingtalkRedirectUri);
} else {
console.warn("⚠️ [Callback] 钉钉登录但未配置钉钉回调地址,使用默认内网地址");
}
} else {
// 内网登录:使用默认内网回调地址
console.log("🔧 [Callback] 使用内网回调地址:", portOAuthConfig.redirectUri);
}
try {
console.log("🔧 开始处理OAuth2.0回调");
+4 -3
View File
@@ -411,7 +411,7 @@ export default function Home() {
<tr className="border-b border-gray-200">
<th className="py-3 px-4 text-left font-medium text-gray-700"></th>
<th className="py-3 px-4 text-left font-medium text-gray-700"></th>
<th className="py-3 px-4 text-right font-medium text-gray-700"></th>
<th className="py-3 px-4 text-right font-medium text-gray-700"></th>
</tr>
</thead>
<tbody>
@@ -430,8 +430,9 @@ export default function Home() {
<td className="py-3 px-4 text-gray-900">{item.point_name}</td>
<td className="py-3 px-4 text-right">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<i className="ri-user-line mr-1"></i>
{item.error_user_count}
{/* <i className="ri-user-line mr-1"></i> */}
{/* <i className="ri-numbers-line mr-1"></i> */}
<p className="text-sm mr-0.5">{item.error_user_count}</p>
</span>
</td>
</tr>
+111
View File
@@ -0,0 +1,111 @@
/**
* Switch 开关组件样式
*/
/* Switch 基础样式 */
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
background-color: #e9ecef;
border-radius: 12px;
cursor: pointer;
transition: background-color 0.3s ease, border-color 0.3s ease;
border: 1px solid #dee2e6;
flex-shrink: 0;
box-sizing: border-box;
padding: 0;
}
/* 移除 button 默认样式 */
.switch::-webkit-inner-spin-button,
.switch::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.switch {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
/* Switch 选中状态 - 绿色 */
.switch-checked {
background-color: #00684a;
border-color: #00684a;
}
.switch-unchecked {
background-color: #e9ecef;
border-color: #dee2e6;
}
/* Switch 滑块 */
.switch-handle {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.switch-handle-checked {
transform: translateX(20px);
}
.switch-handle-unchecked {
transform: translateX(0);
}
/* Switch 禁用状态 */
.switch-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.switch-disabled.switch-checked {
background-color: rgba(0, 104, 74, 0.5);
border-color: rgba(0, 104, 74, 0.5);
}
/* Switch 加载状态 */
.switch-loading {
cursor: wait;
}
/* 加载动画图标 */
.switch-loading-icon {
display: block;
width: 10px;
height: 10px;
border: 2px solid #00684a;
border-top-color: transparent;
border-radius: 50%;
animation: switch-spin 0.8s linear infinite;
}
@keyframes switch-spin {
to {
transform: rotate(360deg);
}
}
/* Hover 效果(仅非禁用状态) */
.switch:not(.switch-disabled):hover {
box-shadow: 0 0 0 4px rgba(0, 104, 74, 0.1);
}
/* Focus 样式 */
.switch:focus-visible {
outline: 2px solid #00684a;
outline-offset: 2px;
}
+2 -1
View File
@@ -26,7 +26,8 @@ module.exports = {
NEXT_PUBLIC_PORT: '51703',
NEXT_PUBLIC_CLIENT_ID: 'meizhou',
NEXT_PUBLIC_API_PORT_CONFIG: '51703',
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
OAUTH_CLIENT_SECRET: '8LRtfPB6dI6PoRJ53b9pJSagB6UOns2Ss3biO8mTCZ',
DINGTALK_REDIRECT_URI_51703: 'https://10-79-97-1751703-b3qrpmf0jy80t3.ztna-dingtalk.com/callback'
},
error_file: './logs/meizhou-err.log',
out_file: './logs/meizhou-out.log',