Skip to content

优化微信扫码登录流程,扫码直接登录,无需再次输入验证码 - #2508

Closed
feitianbubu wants to merge 4850 commits into
QuantumNous:mainfrom
feitianbubu:pr/add-qr-login-no-need-verification-code
Closed

优化微信扫码登录流程,扫码直接登录,无需再次输入验证码#2508
feitianbubu wants to merge 4850 commits into
QuantumNous:mainfrom
feitianbubu:pr/add-qr-login-no-need-verification-code

Conversation

@feitianbubu

@feitianbubu feitianbubu commented Dec 24, 2025

Copy link
Copy Markdown
Member

之前的微信登录, 需要扫码后, 通过公众号获得的验证码登录, 比较繁琐
本次优化, 通过每次生成带自定义值的二维码, 扫码后由服务端实现用户id识别, 避免用户再次输入验证码的流程,实现扫码即登录
原理:
生成带场景值二维码-->用户扫码-->接收微信通知:服务端场景值和openid对应-->前端轮询是否扫码成功-->成功登录

效果:
image

说明:
由于去掉验证码的功能是基于服务端对二维码场景值和微信openid绑定关系的验证,
因此需要升级原来的wechat-server, 我已提pr给songquanpeng大佬:songquanpeng/wechat-server#22
但由于大佬已不怎么维护此项目, 在它合并之前也可以使用我fork的仓库: https://github.com/feitianbubu/wechat-server

Summary by CodeRabbit

  • New Features
    • Added WeChat QR code direct login method with real-time status polling
    • Introduced system setting to enable/disable WeChat QR code direct login
    • Enhanced WeChat account binding with QR code-based authentication flow
    • Expanded multi-language support for new feature (English, French, Japanese, Russian, Vietnamese, Chinese)

✏️ Tip: You can customize this high-level summary in your review settings.

Calcium-Ion and others added 30 commits November 5, 2025 16:02
feat:  EditTokenModal 中针对用户创建的 token 默认无限额度
feat: add environment variable switch for critical rate limit
…80p-image

fix: trim suffix p for jimeng image model
…odisable

fix(channel): 当没有可用密钥时返回错误而不是第一个密钥
… new sections for partners, acknowledgments, and deployment instructions
- 更新中文README.md中的语言链接
- 完全重写英文README.en.md,包含所有详细功能说明
- 完全重写法文README.fr.md,确保内容一致性
- 完全重写日文README.ja.md,提供完整的项目说明

所有语言版本现在具有:
- 相同的结构和格式
- 一致的语言导航
- 完整的功能特性和部署指南
- 统一的环境变量配置说明
…ence

修复viduq2不支持参考生视频的问题
…annel

feat: replicate channel flux model
Calcium-Ion and others added 26 commits December 13, 2025 19:14
…itelist-cidr

feat(auth): enhance IP restriction handling with CIDR support
… meta when token count is disabled

Clamp request body size (including post-decompression) to avoid memory exhaustion caused by huge payloads/zip bombs, especially with large-context Claude requests. Add a configurable `MAX_REQUEST_BODY_MB` (default `32`) and document it.

- Enforce max request body size after gzip/br decompression via `http.MaxBytesReader`
- Add a secondary size guard in `common.GetRequestBody` and cache-safe handling
- Return **413 Request Entity Too Large** on oversized bodies in relay entry
- Avoid building large `TokenCountMeta.CombineText` when both token counting and sensitive check are disabled (use lightweight meta for pricing)
- Update READMEs (CN/EN/FR/JA) with `MAX_REQUEST_BODY_MB`
- Fix a handful of vet/formatting issues encountered during the change
- `go test ./...` passes
Tighten oversized request handling across relay paths and make error matching reliable.

- Align `MAX_REQUEST_BODY_MB` fallback to `32` in request body reader and decompression middleware
- Stop ignoring `GetRequestBody` errors in relay retry paths; return consistent **413** on oversized bodies (400 for other read errors)
- Add `Unwrap()` to `types.NewAPIError` so `errors.Is/As` can match wrapped underlying errors
- `go test ./...` passes
…-response-id

fix: 模型设置增加针对Vertex渠道过滤content[].part[].functionResponse.id的选项,默认启用
- Replace legacy `docs.newapi.pro` paths with the new `/{lang}/docs/...` structure across all README translations
- Point key sections (installation, env vars, API, support, features) to their new locations
- Ensure language-specific links use the correct locale prefix (zh/en/ja) and keep FR aligned with English routes
Keep new-site links (/{lang}/docs/...) where matching pages exist in the current docs repo
Revert links that have no equivalent in the new docs to the legacy paths on doc.newapi.pro:
Google Gemini Chat
Midjourney-Proxy image docs
Suno music docs
Apply the same rule consistently across all README translations (zh/en/ja/fr)
…te-doc-links-new-routing

🔗 docs(readme): update documentation links to new site routing
@coderabbitai

coderabbitai Bot commented Dec 24, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds a new WeChat direct login feature using QR codes. Backend changes include a new feature flag constant, two proxy API endpoints for QR code generation and login status polling, option management support, and API routing. Frontend changes introduce dual-mode authentication flows for both login and account binding, with QR code display, status polling logic, and UI rendering conditionally based on the feature flag. Localization support is added across six languages.

Changes

Cohort / File(s) Summary
Backend Feature Flag & Option Management
common/constants.go, model/option.go
Added WeChatDirectLoginEnabled boolean flag; integrated into option initialization and update functions for runtime configuration.
Backend API Status Endpoint
controller/misc.go
Extended GetStatus response payload with wechat_direct_login_enabled field mapping to the new flag.
Backend WeChat Handlers
controller/wechat.go
Added two new HTTP proxy handlers: CreateLoginQRCode (POST) generates QR codes via wechat-server, and GetLoginStatus (GET) polls login status with authorization header and timeout handling. Both validate feature flags and return forwarded responses.
Backend API Routes
router/api-router.go
Registered two new endpoints: POST /api/wechat/create_login_qrcode and GET /api/wechat/login_status with CriticalRateLimit middleware.
Frontend Login Flow
web/src/components/auth/LoginForm.jsx
Enhanced login modal with dual-mode operation: added directLoginData state, generateDirectLoginQRCode, pollLoginStatus, and loginWithAuthCode functions for direct QR login; refactored rendering into renderDirectLogin and renderVerificationLogin helpers; conditioned mode selection on feature flag.
Frontend Binding Flow
web/src/components/settings/personal/modals/WeChatBindModal.jsx
Implemented direct WeChat binding with QR code and polling; added state for loginToken, qrcodeUrl, polling, bindStatus; integrated dual-mode binding logic; added onBindSuccess callback prop support; enhanced cleanup on unmount.
Frontend Settings & Data Refresh
web/src/components/settings/SystemSetting.jsx, web/src/components/settings/PersonalSetting.jsx
Exposed WeChatDirectLoginEnabled option in SystemSetting UI with toggle control; added post-bind data refresh in PersonalSetting via handleWeChatBindSuccess.
Localization
web/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json
Added "启用微信扫码直接登录" translation entries across six language locales.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant Client as Browser Client
    participant API as Backend API
    participant WSvr as WeChat Server
    
    rect rgb(200, 230, 255)
    Note over User,WSvr: Direct WeChat QR Code Login Flow
    User->>Client: Click WeChat Login
    Client->>API: POST /api/wechat/create_login_qrcode
    API->>WSvr: POST create QR code (wechat-server proxy)
    WSvr-->>API: {login_token, qrcode_url}
    API-->>Client: {login_token, qrcode_url}
    Client->>Client: Display QR code, start polling
    end
    
    rect rgb(240, 200, 230)
    Note over User,WSvr: Polling Phase (repeats every 2s, max 10 min)
    User->>User: Scan QR on WeChat client
    Client->>API: GET /api/wechat/login_status?login_token=...
    API->>WSvr: GET fetch login status (wechat-server proxy)
    alt User authorized on WeChat
        WSvr-->>API: {auth_code}
        API-->>Client: {auth_code}
        Client->>Client: Stop polling
        Client->>API: POST /login with auth_code
        API-->>Client: {session_token}
        Client->>Client: Login successful, navigate home
    else Poll timeout (10 min exceeded)
        Client->>Client: Show error, stop polling
    else Pending (no user action yet)
        WSvr-->>API: {status: 'pending'}
        API-->>Client: {status: 'pending'}
        Client->>Client: Continue polling
    end
    end
Loading
sequenceDiagram
    autonumber
    actor User
    participant Client as Browser Client
    participant API as Backend API
    participant WSvr as WeChat Server
    
    rect rgb(200, 230, 255)
    Note over User,WSvr: Direct WeChat Binding Flow
    User->>Client: Open WeChat Bind Modal
    Client->>API: POST /api/wechat/create_login_qrcode
    API->>WSvr: POST create QR code
    WSvr-->>API: {login_token, qrcode_url}
    API-->>Client: {login_token, qrcode_url}
    Client->>Client: Display QR code, start polling
    end
    
    rect rgb(240, 200, 230)
    Note over User,WSvr: Polling & Binding
    User->>User: Scan QR on WeChat
    loop Poll Every 2s
        Client->>API: GET /api/wechat/login_status?login_token=...
        API->>WSvr: GET fetch status
        WSvr-->>API: status response
        API-->>Client: status response
    end
    
    alt User authorized
        Client->>Client: Stop polling
        Client->>API: POST /bind with auth_code
        API-->>Client: Bind success
        Client->>Client: Trigger onBindSuccess callback
        Client->>API: GET /getUserData (refresh)
        API-->>Client: Updated user info
        Client->>Client: Update local state, close modal
    else Expired or Error
        Client->>Client: Show error message, stop polling
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • seefs001

Poem

🐰 A rabbit hops through WeChat gates,
With QR codes and polling states,
Direct login flows, no delays,
Binding flourishes in many ways!
From zh to en, translations align—
Whiskers twitch at changes divine!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: optimizing the WeChat QR code login flow to enable direct login without requiring verification code entry.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/components/settings/PersonalSetting.jsx (1)

302-319: Redundant getUserData() calls after WeChat bind.

The bindWeChat function calls getUserData() at line 311, and the new handleWeChatBindSuccess callback (passed to WeChatBindModal at line 513) also calls getUserData() at line 318. This results in two identical API calls when WeChat binding succeeds.

Recommendation: Remove the getUserData() call from bindWeChat (line 311) since the callback pattern now handles the refresh. The WeChatBindModal should trigger onBindSuccess after a successful bind, making the call in bindWeChat unnecessary.

🔎 Suggested fix
 const bindWeChat = async () => {
   if (inputs.wechat_verification_code === '') return;
   const res = await API.get(
     `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
   );
   const { success, message } = res.data;
   if (success) {
     showSuccess(t('微信账户绑定成功!'));
     setShowWeChatBindModal(false);
-    await getUserData();
   } else {
     showError(message);
   }
 };
🧹 Nitpick comments (4)
web/src/i18n/locales/vi.json (1)

559-559: Vietnamese translation is correct; optional nuance improvement

The new string is accurate and consistent with other “启用…” → “Bật …” settings.
If you want slightly more natural Vietnamese, you could consider:

Optional wording tweak
- "启用微信扫码直接登录": "Bật đăng nhập trực tiếp bằng mã QR WeChat",
+ "启用微信扫码直接登录": "Bật đăng nhập trực tiếp bằng quét mã QR WeChat",
web/src/i18n/locales/ja.json (1)

559-559: Japanese label is correct and consistent with other “启用…” toggles

"WeChatスキャンコード直接ログインを有効にする" accurately reflects “启用微信扫码直接登录” and matches the “…を有効にする” pattern used elsewhere. If you want slightly more natural phrasing later, something like WeChatのQRコードによる直接ログインを有効にする would also work, but it’s not required.

web/src/i18n/locales/en.json (1)

598-598: English translation is accurate; optional micro‑polish

“Enable WeChat QR Code Direct Login” is clear and consistent with other “Enable …” toggles. If you want slightly more natural English, you could use e.g. “Enable direct WeChat QR code login” later, but current wording is perfectly acceptable.

web/src/components/settings/personal/modals/WeChatBindModal.jsx (1)

84-107: Consider extracting shared QR code polling logic.

The QR code generation and polling logic in WeChatBindModal.jsx and LoginForm.jsx are nearly identical. Consider extracting this into a custom hook (e.g., useWeChatQRCodeLogin) to reduce duplication and ensure consistent behavior across both components.

Also applies to: 109-143

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42109c5 and 27773ad.

📒 Files selected for processing (15)
  • common/constants.go
  • controller/misc.go
  • controller/wechat.go
  • model/option.go
  • router/api-router.go
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/settings/PersonalSetting.jsx
  • web/src/components/settings/SystemSetting.jsx
  • web/src/components/settings/personal/modals/WeChatBindModal.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh.json
🧰 Additional context used
🧬 Code graph analysis (6)
controller/misc.go (1)
common/constants.go (1)
  • WeChatDirectLoginEnabled (92-92)
model/option.go (1)
common/constants.go (2)
  • OptionMap (37-37)
  • WeChatDirectLoginEnabled (92-92)
web/src/components/settings/personal/modals/WeChatBindModal.jsx (2)
web/src/components/auth/LoginForm.jsx (3)
  • status (110-113)
  • inputs (62-66)
  • inputs (67-67)
web/src/components/settings/PersonalSetting.jsx (4)
  • status (61-61)
  • showWeChatBindModal (63-63)
  • inputs (52-60)
  • bindWeChat (302-315)
controller/wechat.go (2)
common/constants.go (4)
  • WeChatAuthEnabled (48-48)
  • WeChatDirectLoginEnabled (92-92)
  • WeChatServerAddress (89-89)
  • WeChatServerToken (90-90)
types/set.go (1)
  • Set (3-5)
web/src/components/auth/LoginForm.jsx (2)
web/src/components/auth/RegisterForm.jsx (4)
  • onWeChatLoginClicked (145-149)
  • status (103-106)
  • turnstileEnabled (70-70)
  • turnstileToken (72-72)
web/src/components/settings/PersonalSetting.jsx (3)
  • status (61-61)
  • turnstileEnabled (66-66)
  • turnstileToken (68-68)
router/api-router.go (2)
middleware/rate-limit.go (1)
  • CriticalRateLimit (104-109)
controller/wechat.go (2)
  • CreateLoginQRCode (172-217)
  • GetLoginStatus (220-273)
🔇 Additional comments (14)
model/option.go (1)

103-103: LGTM! Consistent option handling for WeChat direct login.

The implementation correctly follows the established pattern for boolean configuration options:

  • Initialized in InitOptionMap using strconv.FormatBool
  • Updated in updateOptionMap using the value == "true" check
  • Consistent with other similar options like WeChatAuthEnabled

Also applies to: 382-383

controller/misc.go (1)

67-67: LGTM! Feature flag correctly exposed in status endpoint.

The new wechat_direct_login_enabled field is properly added to the status response, following the same pattern as other authentication flags (e.g., wechat_login, telegram_oauth).

router/api-router.go (1)

39-41: LGTM! WeChat direct login routes properly configured.

The two new API endpoints are correctly set up:

  • Appropriate rate limiting with CriticalRateLimit middleware
  • Logically grouped with existing WeChat OAuth routes
  • REST conventions followed (POST for creation, GET for status check)
common/constants.go (1)

92-92: LGTM! Safe default for WeChat direct login feature flag.

The constant is properly defined with:

  • Sensible default value (false) requiring explicit opt-in
  • Clear documentation comment explaining the toggle behavior
  • Appropriate placement alongside related WeChat configuration
web/src/i18n/locales/fr.json (1)

601-601: LGTM! French localization added for WeChat direct login feature.

The translation for "启用微信扫码直接登录" → "Activer la connexion directe par code QR WeChat" is properly added and maintains consistency with the file's formatting.

web/src/i18n/locales/ru.json (1)

605-605: Russian translation matches semantics and existing style

“Включить прямой вход через QR-код WeChat” correctly reflects the source and is consistent with other “启用…” settings strings in this locale.

web/src/i18n/locales/zh.json (1)

593-593: Chinese entry matches key and existing toggle style

The new key/value "启用微信扫码直接登录" is consistent with other “启用…” toggles and correctly describes the feature. No changes needed.

web/src/components/settings/SystemSetting.jsx (2)

79-79: LGTM!

The WeChatDirectLoginEnabled option is correctly added to the inputs state following the same pattern as other boolean options.


1080-1088: LGTM!

The checkbox is correctly wired to the WeChatDirectLoginEnabled option with proper handleCheckboxChange integration. The label appropriately describes the feature ("启用微信扫码直接登录").

controller/wechat.go (1)

171-217: LGTM!

The CreateLoginQRCode handler correctly validates both feature flags, sets appropriate headers, uses a reasonable timeout, and handles errors properly.

web/src/components/settings/personal/modals/WeChatBindModal.jsx (2)

109-143: LGTM!

The polling logic is well-implemented with proper cleanup on all exit paths (success, expired, max polls reached). The 2-second interval and 10-minute maximum duration are reasonable for this use case.


145-165: LGTM!

The bindWithAuthCode function correctly handles success/error cases, invokes onBindSuccess callback for parent notification, and uses a slight delay before closing the modal to allow the success message to be visible.

web/src/components/auth/LoginForm.jsx (2)

77-81: LGTM!

The directLoginData state structure is clean and appropriate for tracking the QR code login flow.


928-963: LGTM!

The renderDirectLogin function correctly displays the QR code, polling status, and refresh button. The loading state handling is appropriate.

Comment thread controller/wechat.go
Comment on lines +229 to +239
loginToken := c.Query("login_token")
if loginToken == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "缺少login_token参数",
})
return
}

// 代理请求到wechat-server
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, loginToken), nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

URL-encode the loginToken query parameter to prevent injection.

The loginToken is user-supplied input directly interpolated into the URL. If it contains special characters (e.g., &, #, ?), it could break the URL structure or potentially be exploited.

🔎 Proposed fix
+import "net/url"
+
 // 代理查询登录状态请求
 func GetLoginStatus(c *gin.Context) {
 	if !common.WeChatAuthEnabled || !common.WeChatDirectLoginEnabled {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
 			"message": "管理员未开启微信扫码直接登录功能",
 		})
 		return
 	}
 
 	loginToken := c.Query("login_token")
 	if loginToken == "" {
 		c.JSON(http.StatusBadRequest, gin.H{
 			"success": false,
 			"message": "缺少login_token参数",
 		})
 		return
 	}
 
 	// 代理请求到wechat-server
-	req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, loginToken), nil)
+	req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, url.QueryEscape(loginToken)), nil)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loginToken := c.Query("login_token")
if loginToken == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "缺少login_token参数",
})
return
}
// 代理请求到wechat-server
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, loginToken), nil)
import "net/url"
loginToken := c.Query("login_token")
if loginToken == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "缺少login_token参数",
})
return
}
// 代理请求到wechat-server
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, url.QueryEscape(loginToken)), nil)
🤖 Prompt for AI Agents
In controller/wechat.go around lines 229 to 239, the code interpolates the
user-supplied loginToken directly into the request URL; URL-encode the
loginToken to prevent malformed URLs or injection by using net/url (e.g.,
url.QueryEscape(loginToken)) or build the URL via url.URL / url.Values and set
the query parameter safely before creating the http.NewRequest; replace the
current fmt.Sprintf(...) interpolation with a safely-encoded query parameter.

Comment on lines +211 to +241
// 轮询登录状态
const pollLoginStatus = async (loginToken) => {
const interval = setInterval(async () => {
try {
const res = await API.get(`/api/wechat/login_status?login_token=${loginToken}`);
if (res.data.success) {
const { status, auth_code } = res.data.data;

if (status === 'success' && auth_code) {
clearInterval(interval);
setDirectLoginData(prev => ({ ...prev, polling: false }));

// 使用授权码完成登录
await loginWithAuthCode(auth_code);
} else if (status === 'expired') {
clearInterval(interval);
setDirectLoginData(prev => ({ ...prev, polling: false }));
showError('二维码已过期,请重新生成');
}
}
} catch (error) {
console.error('轮询失败:', error);
}
}, 2000); // 每2秒轮询一次

// 10分钟后停止轮询
setTimeout(() => {
clearInterval(interval);
setDirectLoginData(prev => ({ ...prev, polling: false }));
}, 10 * 60 * 1000);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memory leak: polling interval is not cleaned up on component unmount.

The pollLoginStatus function stores the interval in a local variable that cannot be accessed for cleanup when the component unmounts. If the user navigates away while polling is active, the interval continues running and may cause state updates on an unmounted component.

🔎 Proposed fix using a ref

Add a ref at the component level:

const pollingIntervalRef = useRef(null);
const pollingTimeoutRef = useRef(null);

Update the cleanup in a useEffect:

useEffect(() => {
  return () => {
    if (pollingIntervalRef.current) {
      clearInterval(pollingIntervalRef.current);
    }
    if (pollingTimeoutRef.current) {
      clearTimeout(pollingTimeoutRef.current);
    }
  };
}, []);

Then modify pollLoginStatus:

 const pollLoginStatus = async (loginToken) => {
-  const interval = setInterval(async () => {
+  pollingIntervalRef.current = setInterval(async () => {
     try {
       const res = await API.get(`/api/wechat/login_status?login_token=${loginToken}`);
       if (res.data.success) {
         const { status, auth_code } = res.data.data;

         if (status === 'success' && auth_code) {
-          clearInterval(interval);
+          clearInterval(pollingIntervalRef.current);
+          pollingIntervalRef.current = null;
           setDirectLoginData(prev => ({ ...prev, polling: false }));
           await loginWithAuthCode(auth_code);
         } else if (status === 'expired') {
-          clearInterval(interval);
+          clearInterval(pollingIntervalRef.current);
+          pollingIntervalRef.current = null;
           setDirectLoginData(prev => ({ ...prev, polling: false }));
           showError('二维码已过期,请重新生成');
         }
       }
     } catch (error) {
       console.error('轮询失败:', error);
     }
   }, 2000);

-  setTimeout(() => {
-    clearInterval(interval);
+  pollingTimeoutRef.current = setTimeout(() => {
+    if (pollingIntervalRef.current) {
+      clearInterval(pollingIntervalRef.current);
+      pollingIntervalRef.current = null;
+    }
     setDirectLoginData(prev => ({ ...prev, polling: false }));
   }, 10 * 60 * 1000);
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In web/src/components/auth/LoginForm.jsx around lines 211 to 241, the polling
interval and timeout are created locally in pollLoginStatus and not cleaned up
on component unmount, causing potential memory leaks and state updates on
unmounted components; fix by adding component-level refs (e.g.,
pollingIntervalRef and pollingTimeoutRef) to store the interval ID and timeout
ID, assign interval/timeout IDs to those refs inside pollLoginStatus instead of
local variables, update pollLoginStatus to check the component is still mounted
before calling setState or use a mounted flag if needed, and add a useEffect
cleanup that clears pollingIntervalRef.current and pollingTimeoutRef.current if
present to ensure timers are cancelled on unmount.

Comment on lines +965 to +990
// 微信登录模态框
const renderWeChatLoginModal = () => {
// 根据 WeChatDirectLoginEnabled 决定显示哪种登录方式
const isDirectLogin = status.wechat_direct_login_enabled;

return (
<Modal
title={isDirectLogin ? t('扫码直接登录') : t('验证码登录')}
visible={showWeChatLoginModal}
maskClosable={true}
onCancel={() => {
setShowWeChatLoginModal(false);
setDirectLoginData({ loginToken: '', qrcodeUrl: '', polling: false });
}}
centered={true}
footer={isDirectLogin ? null : undefined}
onOk={isDirectLogin ? undefined : onSubmitWeChatVerificationCode}
okText={isDirectLogin ? undefined : t('登录')}
okButtonProps={isDirectLogin ? undefined : {
loading: wechatCodeSubmitLoading,
}}
>
{isDirectLogin ? renderDirectLogin() : renderVerificationLogin()}
</Modal>
);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Nested Modal bug when using verification login mode.

When isDirectLogin is false, renderWeChatLoginModal() renders a <Modal> that contains the result of renderVerificationLogin(), which itself returns another <Modal> (lines 891-925). This creates a nested Modal structure.

🔎 Proposed fix

Option 1: Have renderVerificationLogin return only the content, not a Modal:

 const renderVerificationLogin = () => {
   return (
-    <Modal
-          title={t('微信扫码登录')}
-          visible={showWeChatLoginModal}
-          maskClosable={true}
-          onOk={onSubmitWeChatVerificationCode}
-          onCancel={() => setShowWeChatLoginModal(false)}
-          okText={t('登录')}
-          centered={true}
-          okButtonProps={{
-              loading: wechatCodeSubmitLoading,
-          }}
-    >
+    <>
       <div className='flex flex-col items-center mb-4'>
         <img src={status.wechat_qrcode} alt='微信二维码' className='mb-4 w-32 h-32' />
       </div>
       ...
-    </Modal>
+    </>
   );
 };

Option 2: Remove renderWeChatLoginModal wrapper and call renderVerificationLogin directly in the component return when not using direct login.

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.