1. 添加新的正式环境的secret配置信息。
2. 动态回调地址,如果是钉钉应用则用对应的回调地址。 3. 高频错误评查点改成显示出错次数。 4. 添加开关的通用组件,评查点列表方便修改状态。
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
@@ -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', // 占位符,实际值从环境变量获取
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user