Skip to content

feat(channel): 添加2FA验证后查看渠道密钥功能 - #1653

Merged
creamlike1024 merged 2 commits into
QuantumNous:alphafrom
AAEE86:alpha
Aug 26, 2025
Merged

feat(channel): 添加2FA验证后查看渠道密钥功能#1653
creamlike1024 merged 2 commits into
QuantumNous:alphafrom
AAEE86:alpha

Conversation

@AAEE86

@AAEE86 AAEE86 commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

关闭 #1640

  • 新增接口通过2FA验证后获取渠道密钥
  • 统一实现2FA验证码和备用码的验证逻辑
  • 记录用户查看密钥的操作日志
  • 编辑渠道弹窗新增查看密钥按钮,触发2FA验证模态框
  • 使用TwoFactorAuthModal进行验证码输入及验证
  • 验证成功后弹出渠道密钥展示窗口
  • 对渠道编辑模态框的状态进行了统一重置优化
  • 添加相关国际化文案支持密钥查看功能
示例图片 示例图片
图片1 图片2
图片3 图片4
图片5 图片6

Summary by CodeRabbit

  • New Features
    • Admins can securely view a channel’s key after two‑factor verification (supports TOTP or backup codes); access is audited.
    • Added a reusable 2FA verification modal with code input, loading state, Enter-to-submit, and verification handling.
    • Added a channel key display UI supporting single/multiple keys, per-key copy, copy-all, JSON tagging, and safety notices.
    • “View Key” action integrated into the channel edit modal with a guided verification flow.
  • Localization
    • New translations for key viewing, copying, security prompts, and success/error messages.

- 新增接口通过2FA验证后获取渠道密钥
- 统一实现2FA验证码和备用码的验证逻辑
- 记录用户查看密钥的操作日志
- 编辑渠道弹窗新增查看密钥按钮,触发2FA验证模态框
- 使用TwoFactorAuthModal进行验证码输入及验证
- 验证成功后弹出渠道密钥展示窗口
- 对渠道编辑模态框的状态进行了统一重置优化
- 添加相关国际化文案支持密钥查看功能
@coderabbitai

coderabbitai Bot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a 2FA-gated API endpoint to reveal channel keys (TOTP or backup codes), centralizes 2FA validation, registers a new POST route, and adds frontend pieces: a reusable 2FA modal, a channel key display, EditChannel integration for viewing keys, and related i18n strings.

Changes

Cohort / File(s) Summary
Backend: Channel key API & 2FA
controller/channel.go
Adds GetChannelKey handler requiring 2FA to return a channel key, adds internal validateTwoFactorAuth, and introduces multi-key types: MultiKeyManageRequest, MultiKeyStatusResponse, KeyStatus.
Backend: Routing
router/api-router.go
Registers POST /api/channel/:id/key under channel routes, protected by admin auth and rate limiting, mapped to controller.GetChannelKey.
Frontend: 2FA modal
web/src/components/common/modals/TwoFactorAuthModal.jsx
Adds reusable two-factor modal component with translations, input, enter-to-submit, loading, and cancel/verify actions.
Frontend: Channel key display UI
web/src/components/common/ui/ChannelKeyDisplay.jsx
Adds component to parse/display single/multi/JSON keys, per-key and copy-all actions, notices, and optional security warning (i18n).
Frontend: Edit Channel modal integration
web/src/components/table/channels/modals/EditChannelModal.jsx
Integrates 2FA flow to request key viewing, calls new API, shows key via ChannelKeyDisplay, and centralizes modal/2FA state resets.
Frontend: i18n
web/src/i18n/locales/en.json
Adds English translations for 2FA prompts, key display/copy messages, warnings, and success/error strings; minor formatting tweak.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant UI as Web UI (EditChannelModal)
  participant API as API Server
  participant Auth as TwoFactor Validator
  participant DB as Data Store

  Admin->>UI: Click "View key"
  UI->>Admin: Show TwoFactorAuthModal
  Admin->>UI: Enter code
  UI->>API: POST /api/channel/:id/key { code }
  API->>Auth: validateTwoFactorAuth(userId, code)
  alt code valid (TOTP or backup)
    Auth-->>API: validated
    API->>DB: Fetch channel including key
    DB-->>API: Channel + key
    API-->>UI: 200 { key data }
    UI->>UI: Render ChannelKeyDisplay
  else invalid code
    Auth-->>API: invalid
    API-->>UI: 4xx error
    UI->>Admin: Show error notification
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit tiptoes to a locked-away tree,
Two hops, one code — a tiny victory.
Backup clover and TOTP gleam,
Keys revealed like a dawn-time dream.
Copy swift, then hide with glee. 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@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

🧹 Nitpick comments (15)
web/src/components/common/modals/TwoFactorAuthModal.jsx (2)

111-118: Improve mobile input UX for 6‑digit TOTP while preserving 8‑digit backups

Consider nudging numeric input on mobile while still allowing 8‑digit backup codes:

  • Add inputMode="numeric" and pattern="[0-9]*" to hint numeric keypad for TOTPs; backups remain accepted as digits.

Example:

-      <Input
+      <Input
         placeholder={placeholder || t('请输入认证器验证码或备用码')}
         value={code}
         onChange={onCodeChange}
         size="large"
         maxLength={8}
         onKeyDown={handleKeyDown}
         autoFocus
+        inputMode="numeric"
+        pattern="[0-9]*"
       />

56-87: Modal accessibility nit: mark decorative SVG as aria-hidden

The lock icon in the title is decorative. Add aria-hidden="true" to avoid it being read by screen readers.

-          <div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3">
-            <svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
+          <div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3">
+            <svg aria-hidden="true" className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
               <path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
             </svg>
           </div>

Repeat for other decorative SVGs within this modal if applicable.

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

2016-2020: Polish English translations for consistency and clarity

Minor phrasing tweaks to sound more natural in English.

-  "获取密钥失败": "Failed to get key",
+  "获取密钥失败": "Failed to retrieve key",
   "查看密钥": "View key",
-  "查看渠道密钥": "View channel key",
+  "查看渠道密钥": "View Channel Key",
   "渠道密钥信息": "Channel key information",
-  "密钥获取成功": "Key acquisition successful"
+  "密钥获取成功": "Key retrieved successfully"

2012-2015: Ensure i18n consistency by reusing existing 2FA keys

The new strings introduced at lines 2013–2015 overlap with existing translations elsewhere in en.json. To reduce duplication and simplify maintenance, please reuse or merge with the following keys:

• Replace the standalone “验证” (line 2013) with the existing “验证身份” key (line 1926).
• Instead of “为了保护账户安全,请验证您的两步验证码。” (line 2014), reuse the copy under key “为了保护您的账户安全,请输入认证器验证码来确认身份” (line 1927).
• For the description “支持6位TOTP验证码或8位备用码” (line 2015), consider whether the existing prompt “请输入验证码或备用码” (line 1968) suffices; if a more instructional tooltip is required, merge both contexts under a single new or existing key.

By consolidating these, you’ll avoid near-identical entries and streamline future updates to our 2FA flow.

web/src/components/common/ui/ChannelKeyDisplay.jsx (3)

92-94: Avoid unnecessary re-parsing on re-renders

parseChannelKeys runs on every render; memoize it by keyData to avoid work on large key blobs.

-import React from 'react';
+import React, { useMemo } from 'react';
...
-  const parsedKeys = parseChannelKeys(keyData, t);
+  const parsedKeys = useMemo(() => parseChannelKeys(keyData, t), [keyData, t]);

Also applies to: 20-23


151-153: Hardcoded “JSON” tag bypasses localization

You’re calling t('JSON'), but there’s no explicit key. Either:

  • Add "JSON": "JSON" to locales (so other locales can localize if needed), or
  • Use a plain string if you don’t plan to translate it.

Example i18n addition:

// en.json
+  "JSON": "JSON",

95-103: Copy actions: consider trimming trailing whitespace before copying

Some upstream keys may contain accidental trailing newlines. Trimming before copying prevents surprises.

-  const handleCopyAll = () => {
-    copy(keyData);
+  const handleCopyAll = () => {
+    copy((keyData || '').trim());
     showSuccess(t('所有密钥已复制到剪贴板'));
   };
-  const handleCopyKey = (content) => {
-    copy(content);
+  const handleCopyKey = (content) => {
+    copy((content || '').trim());
     showSuccess(t('密钥已复制到剪贴板'));
   };
controller/channel.go (3)

445-460: TOTP path ignores validation errors; treat errors explicitly and reduce side channels

You’re ignoring the error returned by ValidateTOTPAndUpdateUsage. Handle it to avoid masking internal problems and to keep behavior consistent.

-  if cleanCode, err := common.ValidateNumericCode(code); err == nil {
-    if isValid, _ := twoFA.ValidateTOTPAndUpdateUsage(cleanCode); isValid {
+  if cleanCode, err := common.ValidateNumericCode(code); err == nil {
+    if isValid, err := twoFA.ValidateTOTPAndUpdateUsage(cleanCode); err == nil && isValid {
       return true
     }
   }

432-438: Audit logging: include minimal context for traceability

Log already records userId via context; consider adding success/failure outcome and remote IP (if available) to aid incident investigations while still avoiding sensitive data in logs.

Example:

-  model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
+  model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息成功 (渠道ID: %d)", channelId))

And on failures (above returns), optionally RecordLog with “失败” if your logging policy allows.


436-443: Success message should be i18n-neutral at API layer

The API currently returns "验证成功" (Chinese). Consider returning a stable, language-neutral message or code and let the frontend localize strings.

-    "message": "验证成功",
+    "message": "",
+    "code": "ok",

Or reuse existing ApiSuccess helper consistently.

web/src/components/table/channels/modals/EditChannelModal.jsx (5)

165-173: Trim redundant 2FA state: remove unused code field and simplify modal visibility flags

  • twoFAState.code is never read; the verification flow uses the separate verifyCode state. Keeping it risks confusion and accidental divergence.
  • Optional: collapse showModal/showKey to a single boolean (e.g., keyModalVisible) to avoid double-truth sources for visibility.

Apply this diff to drop the unused code property:

 const [twoFAState, setTwoFAState] = useState({
   showModal: false,
-  code: '',
   loading: false,
   showKey: false,
   keyData: ''
 });
 const resetTwoFAState = () => {
   setTwoFAState({
     showModal: false,
-    code: '',
     loading: false,
     showKey: false,
     keyData: ''
   });
 };

If you prefer a single visibility flag, I can follow up with a compact refactor that switches visible checks to Boolean(twoFAState.keyData) and removes showModal/showKey.

Also applies to: 185-193


544-573: Harden 2FA verification: trim/validate input and avoid duplicate/global error handling

  • Trim whitespace to tolerate pasted codes with spaces.
  • Basic client-side format check reduces unnecessary requests. Assuming backup codes are numeric 8-digits and TOTPs are numeric 6-digits. If your backup codes can include letters, keep only the trim.
  • Use skipErrorHandler: true to prevent interceptor toasts while you handle errors locally; on failure, prefer showError(error) to leverage centralized mapping in showError (e.g., 401 redirect, 429 throttling), per helpers/utils.jsx.
 const handleVerify2FA = async () => {
-  if (!verifyCode) {
+  const code = (verifyCode || '').replace(/\s+/g, '');
+  if (!code) {
     showError(t('请输入验证码或备用码'));
     return;
   }
 
   setVerifyLoading(true);
   try {
-    const res = await API.post(`/api/channel/${channelId}/key`, {
-      code: verifyCode
-    });
+    // 若备份码不严格为数字,请移除下面的正则,仅保留 trim
+    if (!/^(?:\d{6}|\d{8})$/.test(code)) {
+      showError(t('验证码格式不正确,请输入6位动态码或8位备用码'));
+      return;
+    }
+
+    const res = await API.post(
+      `/api/channel/${channelId}/key`,
+      { code },
+      { skipErrorHandler: true }
+    );
     if (res.data.success) {
       // 验证成功,显示密钥
       updateTwoFAState({
         showModal: true,
         showKey: true,
-        keyData: res.data.data.key
+        keyData: res?.data?.data?.key ?? res?.data?.data ?? ''
       });
       reset2FAVerifyState();
       showSuccess(t('验证成功'));
     } else {
-      showError(res.data.message);
+      showError(res?.data?.message || t('验证失败'));
     }
   } catch (error) {
-    showError(t('获取密钥失败'));
+    showError(error);
   } finally {
     setVerifyLoading(false);
   }
 };

576-579: Open 2FA modal with a clean slate

Clear any residual code/loading before showing to avoid edge cases when re-opening quickly.

 const handleShow2FAModal = () => {
-  setShow2FAVerifyModal(true);
+  setVerifyCode('');
+  setVerifyLoading(false);
+  setShow2FAVerifyModal(true);
 };

1170-1179: DRY the “查看密钥” trigger button

The same button appears 3 times. Extract a tiny local component to reduce duplication and ensure consistent behavior/styles.

Add this inside EditChannelModal (so it can use t), near other small helpers:

const ViewKeyButton = React.memo(({ onClick }) => (
  <Button size="small" type="primary" theme="outline" onClick={onClick}>
    {t('查看密钥')}
  </Button>
));

Then replace the three inline buttons with:

-<Button
-  size="small"
-  type="primary"
-  theme="outline"
-  onClick={handleShow2FAModal}
->
-  {t('查看密钥')}
-</Button>
+<ViewKeyButton onClick={handleShow2FAModal} />

Apply the same replacement at Lines 1255-1263 and 1305-1313.

Also applies to: 1255-1263, 1305-1313


1980-2011: Make the key display modal harder to dismiss accidentally (optional) and rely on a single visibility source

Two minor hardening tweaks:

  • Prevent mask/ESC dismissal to reduce accidental closure while reviewing sensitive info.
  • Optional: use a single truth source for visibility (e.g., Boolean(twoFAState.keyData)), which also sidesteps showModal && showKey divergence.
 <Modal
   title={
     <div className="flex items-center">
       ...
     </div>
   }
-  visible={twoFAState.showModal && twoFAState.showKey}
+  visible={twoFAState.showModal && twoFAState.showKey}
+  maskClosable={false}
+  closeOnEsc={false}
   onCancel={resetTwoFAState}
   footer={
     <Button
       type="primary"
       onClick={resetTwoFAState}
     >
       {t('完成')}
     </Button>
   }
   width={700}
   style={{ maxWidth: '90vw' }}
 >

If you decide to consolidate visibility, change the visible prop to Boolean(twoFAState.keyData) and set/clear keyData accordingly on success/cancel.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d374a22 and dc4f575.

📒 Files selected for processing (6)
  • controller/channel.go (1 hunks)
  • router/api-router.go (1 hunks)
  • web/src/components/common/modals/TwoFactorAuthModal.jsx (1 hunks)
  • web/src/components/common/ui/ChannelKeyDisplay.jsx (1 hunks)
  • web/src/components/table/channels/modals/EditChannelModal.jsx (8 hunks)
  • web/src/i18n/locales/en.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
web/src/helpers/utils.jsx (2)
  • showError (113-142)
  • showSuccess (148-150)
🔇 Additional comments (4)
web/src/components/common/ui/ChannelKeyDisplay.jsx (1)

31-72: Robust key parsing with graceful fallback looks good

Good handling of JSON arrays, multiline, and single key inputs with sensible labels and types; the console.warn fallback on JSON parse failure is appropriate.

router/api-router.go (1)

109-137: Route placement and auth look correct

The new POST /api/channel/:id/key lives under AdminAuth within /channel, which aligns with the security posture for viewing secrets.

web/src/components/table/channels/modals/EditChannelModal.jsx (2)

661-685: LGTM: comprehensive modal reset including sensitive state

Good call centralizing all resets, especially clearing keyData on close via resetTwoFAState, which reduces accidental leakage of sensitive info across sessions.


1967-1977: 2FA modal wiring looks correct

Props cover loading state, controlled code input, and explicit cancel path that resets state. No issues spotted.

Comment thread controller/channel.go
Comment thread router/api-router.go Outdated
Comment thread web/src/components/common/modals/TwoFactorAuthModal.jsx Outdated

@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: 1

♻️ Duplicate comments (1)
router/api-router.go (1)

117-117: Resolved: Critical rate limiter added to key-reveal route — LGTM

The sensitive POST /:id/key endpoint is now protected with middleware.CriticalRateLimit() and sits under AdminAuth(). This directly addresses the earlier suggestion to harden the route against brute-force/rapid retries. Good call keeping it POST-only for submitting the 2FA code in the body.

🧹 Nitpick comments (1)
router/api-router.go (1)

117-117: Consider CSRF mitigation for the session-based secret-reveal endpoint

Although your AdminAuth middleware does use cookie-based sessions, you’ve configured the session cookie with SameSite: http.SameSiteStrictMode, which prevents browsers from sending it on cross-site requests, effectively mitigating CSRF on modern browsers. No additional CSRF middleware or token checks were found.

• File to review: router/api-router.go line 117 (channelRoute.POST("/:id/key", …))
• Session store setup in main.go uses SameSite Strict (see lines 142–150)
• No explicit CSRF token generation/validation middleware (e.g. gin-contrib/csrf) detected

Optional recommendation:
• If you need to support older browsers lacking SameSite Strict enforcement, or want defense-in-depth, consider adding an explicit CSRF token check before revealing secrets (e.g. double-submit cookie or hidden form field validated on the server). Rate limiting alone is not a substitute for CSRF protection.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dc4f575 and b1cc905.

📒 Files selected for processing (2)
  • router/api-router.go (1 hunks)
  • web/src/components/common/modals/TwoFactorAuthModal.jsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/common/modals/TwoFactorAuthModal.jsx
🧰 Additional context used
🧬 Code graph analysis (1)
router/api-router.go (1)
controller/channel.go (1)
  • GetChannelKey (384-443)

Comment thread router/api-router.go
@Calcium-Ion Calcium-Ion linked an issue Aug 26, 2025 that may be closed by this pull request
5 tasks
@creamlike1024
creamlike1024 merged commit fcb0339 into QuantumNous:alpha Aug 26, 2025
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Nov 11, 2025
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.

增加使用2fa验证器查看密钥功能

2 participants