Skip to content

feat(web): add settings & pages of privacy policy & user agreement - #1992

Merged
Calcium-Ion merged 4 commits into
QuantumNous:mainfrom
seefs001:pr-upstream-1981
Oct 10, 2025
Merged

feat(web): add settings & pages of privacy policy & user agreement#1992
Calcium-Ion merged 4 commits into
QuantumNous:mainfrom
seefs001:pr-upstream-1981

Conversation

@seefs001

@seefs001 seefs001 commented Oct 10, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added User Agreement and Privacy Policy pages, with links shown during login and registration.
    • Require users to agree before logging in or registering when enabled by admins.
    • Status now indicates whether each policy is enabled.
    • Improved document viewer supporting Markdown, HTML, or external links.
  • Settings

    • Admins can create and update User Agreement and Privacy Policy in Settings, with success/error feedback.
  • Localization

    • Added English, French, and Chinese translations for new policy-related texts.
  • Chores

    • Updated ignore files to exclude Go build cache.

kyubibii and others added 4 commits October 8, 2025 10:43
Extracted the User Agreement and Privacy Policy presentation into a
reusable DocumentRenderer component (web/src/components/common/DocumentRenderer).
Unified rendering logic and i18n source for these documents, removed the
legacy contentDetector utility, and updated the related pages to use the
new component. Adjusted controller/backend (controller/misc.go) and locale
files to support the new rendering approach.

This improves reuse, maintainability, and future extensibility.
@coderabbitai

coderabbitai Bot commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds legal settings storage (user agreement, privacy policy), exposes two public API endpoints to fetch them, surfaces enablement flags in status, updates frontend to render documents and require consent during login/registration, and introduces admin UI and i18n strings to manage/display these documents. Also updates .gitignore/.dockerignore to ignore .gocache.

Changes

Cohort / File(s) Summary of changes
Ignore configs
/.gitignore, /.dockerignore
Ignore Go build cache by adding .gocache to Git and Docker ignore lists.
Backend: legal settings
/setting/system_setting/legal.go
Adds LegalSettings struct, singleton registration, and GetLegalSettings() accessor.
Backend: controllers & routes
/controller/misc.go, /router/api-router.go
GetStatus now includes user_agreement_enabled and privacy_policy_enabled. Adds public endpoints: GET /api/user-agreement, GET /api/privacy-policy.
Frontend: routing
/web/src/App.jsx
Lazily registers routes /user-agreement and /privacy-policy.
Frontend: auth gating
/web/src/components/auth/LoginForm.jsx, /web/src/components/auth/RegisterForm.jsx
Adds consent checkbox, state, and gating for login/registration when agreement/privacy policy enabled; initializes from status; disables actions until agreed.
Frontend: document rendering
/web/src/components/common/DocumentRenderer/index.jsx
New component to fetch, cache, and render legal documents (HTML/URL/Markdown) with sanitization and loading/error states.
Frontend: settings (admin)
/web/src/components/settings/OtherSetting.jsx
Adds fields and submit handlers to edit and save User Agreement and Privacy Policy.
Frontend: pages
/web/src/pages/UserAgreement/index.jsx, /web/src/pages/PrivacyPolicy/index.jsx
New pages rendering documents via DocumentRenderer and calling /api/user-agreement and /api/privacy-policy.
i18n
/web/src/i18n/locales/en.json, /web/src/i18n/locales/fr.json, /web/src/i18n/locales/zh.json
Adds translations for legal documents, consent texts, errors, help messages, and admin labels.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant W as Web App (SPA)
  participant A as API Server
  participant S as System Settings (legal)

  rect rgb(245,248,255)
  note over U,W: App load / status check
  U->>W: Open login/register
  W->>A: GET /api/status
  A->>S: Read LegalSettings
  S-->>A: { user_agreement, privacy_policy }
  A-->>W: { user_agreement_enabled, privacy_policy_enabled, ... }
  end

  rect rgb(240,255,245)
  note over U,W: Consent gating
  alt Consent required
    W->>U: Render checkbox "I agree ..."
    U-->>W: Toggle agreedToTerms = true
    W->>A: Proceed with login/register
  else Not required
    W->>A: Proceed with login/register
  end
  end
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant W as Web App (SPA)
  participant A as API Server
  participant S as System Settings (legal)
  participant C as localStorage (cache)

  rect rgb(255,248,240)
  note over U,W: View legal document
  U->>W: Navigate to /user-agreement or /privacy-policy
  W->>C: Read cache by key
  alt Cache hit
    C-->>W: Cached content
    W->>U: Render content
    par Refresh in background
      W->>A: GET /api/(user-agreement|privacy-policy)
      A->>S: Read content
      S-->>A: Content
      A-->>W: Content
      W->>C: Update cache
    end
  else Cache miss
    W->>A: GET /api/(user-agreement|privacy-policy)
    A->>S: Read content
    S-->>A: Content
    A-->>W: Content or error
    opt Success
      W->>C: Write cache
      W->>U: Render content
    end
    opt Error
      W->>U: Show translated error/empty state
    end
  end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I thump my paw—new scrolls appear,
Agreements clear, policies near.
A checkbox tick before I hop,
Through login fields to carrot shop.
Cached whispers load without a lag—
Legal leaves in my saddlebag.
Boing! Compliance in a tag. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 title succinctly captures the core feature—adding settings and pages for privacy policy and user agreement—reflecting the main changes in the PR without extraneous detail.
✨ 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: 5

🧹 Nitpick comments (3)
web/src/i18n/locales/fr.json (1)

2254-2276: Verify French translations with native speaker.

The new translations for user agreement and privacy policy features are syntactically correct and comprehensive. However, consider having a native French speaker review them for naturalness and cultural appropriateness, especially for legal/consent-related text which requires precise wording.

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

128-131: Consider extracting agreement validation into helper function.

The same agreement check logic is duplicated across 7 different login handlers. While the current implementation is correct, extracting this into a helper function would improve maintainability:

+const validateAgreementConsent = () => {
+  if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
+    showInfo(t('请先阅读并同意用户协议和隐私政策'));
+    return false;
+  }
+  return true;
+};

const onWeChatLoginClicked = () => {
-  if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
-    showInfo(t('请先阅读并同意用户协议和隐私政策'));
-    return;
-  }
+  if (!validateAgreementConsent()) return;
  // ... rest of handler
};

This reduces duplication and makes future updates easier.

Also applies to: 171-174, 226-229, 266-269, 281-284, 296-299, 317-320

web/src/components/common/DocumentRenderer/index.jsx (1)

186-194: Consider enhancing accessibility attributes.

The external link button has basic accessibility, but could be improved with additional ARIA attributes for better screen reader support.

             <a
               href={content.trim()}
               target='_blank'
               rel='noopener noreferrer'
               title={content.trim()}
               aria-label={`${t('访问' + title)}: ${content.trim()}`}
+              role='button'
               className='inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
             >
               {t('访问' + title)}
             </a>

Additionally, consider truncating very long URLs in the title and aria-label attributes to improve readability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f5b942 and fe9b305.

📒 Files selected for processing (15)
  • .dockerignore (1 hunks)
  • .gitignore (1 hunks)
  • controller/misc.go (3 hunks)
  • router/api-router.go (1 hunks)
  • setting/system_setting/legal.go (1 hunks)
  • web/src/App.jsx (2 hunks)
  • web/src/components/auth/LoginForm.jsx (13 hunks)
  • web/src/components/auth/RegisterForm.jsx (5 hunks)
  • web/src/components/common/DocumentRenderer/index.jsx (1 hunks)
  • web/src/components/settings/OtherSetting.jsx (4 hunks)
  • web/src/i18n/locales/en.json (3 hunks)
  • web/src/i18n/locales/fr.json (1 hunks)
  • web/src/i18n/locales/zh.json (1 hunks)
  • web/src/pages/PrivacyPolicy/index.jsx (1 hunks)
  • web/src/pages/UserAgreement/index.jsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
web/src/pages/UserAgreement/index.jsx (2)
web/src/App.jsx (1)
  • UserAgreement (54-54)
web/src/components/common/DocumentRenderer/index.jsx (2)
  • useTranslation (76-76)
  • DocumentRenderer (75-241)
web/src/components/auth/LoginForm.jsx (2)
web/src/components/auth/RegisterForm.jsx (4)
  • agreedToTerms (85-85)
  • hasUserAgreement (86-86)
  • hasPrivacyPolicy (87-87)
  • status (97-100)
web/src/helpers/utils.jsx (2)
  • showInfo (161-163)
  • a (259-259)
router/api-router.go (1)
controller/misc.go (2)
  • GetUserAgreement (157-164)
  • GetPrivacyPolicy (166-173)
web/src/components/settings/OtherSetting.jsx (1)
web/src/helpers/utils.jsx (2)
  • showSuccess (157-159)
  • showError (122-151)
setting/system_setting/legal.go (1)
setting/config/config.go (1)
  • GlobalConfig (18-18)
web/src/components/auth/RegisterForm.jsx (1)
web/src/components/auth/LoginForm.jsx (4)
  • agreedToTerms (87-87)
  • hasUserAgreement (88-88)
  • hasPrivacyPolicy (89-89)
  • status (99-102)
web/src/pages/PrivacyPolicy/index.jsx (2)
web/src/App.jsx (1)
  • PrivacyPolicy (55-55)
web/src/components/common/DocumentRenderer/index.jsx (2)
  • useTranslation (76-76)
  • DocumentRenderer (75-241)
controller/misc.go (4)
setting/system_setting/legal.go (1)
  • GetLegalSettings (19-21)
web/src/pages/UserAgreement/index.jsx (1)
  • UserAgreement (24-35)
web/src/App.jsx (2)
  • UserAgreement (54-54)
  • PrivacyPolicy (55-55)
web/src/pages/PrivacyPolicy/index.jsx (1)
  • PrivacyPolicy (24-35)
web/src/App.jsx (3)
web/src/pages/UserAgreement/index.jsx (1)
  • UserAgreement (24-35)
web/src/pages/PrivacyPolicy/index.jsx (1)
  • PrivacyPolicy (24-35)
web/src/components/common/ui/Loading.jsx (1)
  • Loading (23-29)
web/src/components/common/DocumentRenderer/index.jsx (2)
web/src/helpers/utils.jsx (2)
  • showError (122-151)
  • a (259-259)
web/src/components/common/markdown/MarkdownRenderer.jsx (1)
  • MarkdownRenderer (594-652)
🪛 Biome (2.1.2)
web/src/components/common/DocumentRenderer/index.jsx

[error] 217-217: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 233-233: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (19)
.gitignore (1)

16-16: LGTM!

Adding .gocache to ignore the Go build cache is appropriate and aligns with the corresponding .dockerignore update.

.dockerignore (1)

8-9: LGTM!

Excluding build cache directories from Docker images reduces image size and prevents build artifacts from being included.

web/src/components/settings/OtherSetting.jsx (1)

37-38: LGTM!

The implementation follows established patterns for handling settings updates, including:

  • State management with inputs and loadingInput
  • Async submission with error handling via updateOption
  • User feedback through showSuccess/showError

Also applies to: 44-45, 77-78, 106-149

web/src/pages/UserAgreement/index.jsx (1)

24-35: LGTM!

The component is clean and properly delegates content rendering to DocumentRenderer, which handles fetching, caching, and multiple content formats (HTML, Markdown, URLs).

web/src/App.jsx (1)

54-55: LGTM!

The routes are correctly configured:

  • Lazy loading with React.lazy() for code splitting
  • Suspense with Loading fallback for better UX
  • Public access (no authentication) aligns with the purpose of legal documents

Also applies to: 306-321

router/api-router.go (1)

23-24: LGTM!

The endpoints are correctly positioned with other public content endpoints (/status, /notice, /about) and map to controller methods that return legal settings content.

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

114-136: LGTM!

The translations comprehensively cover the new legal settings feature:

  • Admin UI labels and help text
  • User-facing content (agreement/policy labels)
  • Error and loading states
  • Registration consent language
web/src/pages/PrivacyPolicy/index.jsx (1)

24-35: LGTM! Clean implementation following established patterns.

The PrivacyPolicy component correctly uses DocumentRenderer with appropriate props. The implementation is consistent with the UserAgreement page pattern and properly integrates i18n support.

controller/misc.go (3)

46-46: LGTM! Status endpoint correctly exposes legal document availability.

The addition of legal settings to the status endpoint follows the established pattern and provides the frontend with necessary information to conditionally enforce agreement requirements during authentication flows.

Also applies to: 112-113


157-164: LGTM! Public endpoint appropriately serves user agreement content.

The GetUserAgreement endpoint correctly retrieves and returns the user agreement content. Public access without authentication is appropriate for legal documents that users need to review before creating accounts.


166-173: LGTM! Privacy policy endpoint follows consistent pattern.

The GetPrivacyPolicy endpoint correctly implements the same pattern as GetUserAgreement. The symmetry is appropriate given their similar purpose and usage.

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

40-40: LGTM! State management properly initialized.

The addition of Checkbox import and three state variables for tracking user agreement requirements follows React best practices with proper initialization.

Also applies to: 87-89


109-112: LGTM! Status-based initialization correctly implemented.

The component properly initializes agreement requirements from the status object, with safe fallback values. This allows the backend to control whether agreements are enforced.


676-676: Login button correctly disabled when agreement not accepted.

The email login button is properly disabled when user agreements are required but not yet accepted. Note that the OAuth option buttons (GitHub, OIDC, etc.) are not disabled, but they do have the same validation check in their click handlers. This is acceptable as it provides consistent enforcement.


524-560: Consent Page Routes and Empty Content Handling Confirmed

/user-agreement and /privacy-policy are lazy-loaded in App.jsx via Suspense; both pages use DocumentRenderer with an emptyMessage for missing content; links open in new tabs with noopener noreferrer.

web/src/components/auth/RegisterForm.jsx (3)

33-33: LGTM! Registration form state properly initialized.

The registration form correctly initializes agreement tracking state, following the same pattern as the login form for consistency.

Also applies to: 85-87


113-115: LGTM! Agreement flags correctly initialized from status.

The initialization matches the login form implementation and properly depends on the status object.


515-551: Ensure TOS acceptance is enforced for OAuth signup
Email‐based registration is correctly gated by the consent checkbox, but OAuth flows bypass it. Confirm that the backend OAuth callback validates agreedToTerms (or equivalent) before creating an account via OAuth.

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

247-248: LGTM! Localization entries are well-structured.

The new English translations for user agreement and privacy policy features are properly formatted and follow consistent patterns with existing entries. The keys and values align with the component usage.

Also applies to: 1265-1266, 2267-2282

Comment on lines +1 to +21
package system_setting

import "one-api/setting/config"

type LegalSettings struct {
UserAgreement string `json:"user_agreement"`
PrivacyPolicy string `json:"privacy_policy"`
}

var defaultLegalSettings = LegalSettings{
UserAgreement: "",
PrivacyPolicy: "",
}

func init() {
config.GlobalConfig.Register("legal", &defaultLegalSettings)
}

func GetLegalSettings() *LegalSettings {
return &defaultLegalSettings
}

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.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify thread-safety of config.GlobalConfig.Register.

The init() function registers &defaultLegalSettings with GlobalConfig. Ensure that the config manager's storage and retrieval mechanisms are thread-safe and that updates (via /api/option/) don't directly mutate the registered pointer, as this would compound the race condition flagged above.

Run the following script to examine the config manager's implementation:


🏁 Script executed:

#!/bin/bash
# Verify GlobalConfig implementation for thread-safety
rg -nP --type=go -A10 'type\s+ConfigManager' setting/config/
rg -nP --type=go -A10 'func.*Register\(' setting/config/
rg -nP --type=go -A10 'func.*Get\(' setting/config/

Length of output: 1703


Guard LegalSettings mutations behind a mutex or use atomic updates
ConfigManager.Register/Get use an RWMutex for map safety, but the returned *LegalSettings can still be mutated concurrently without synchronization. Refactor to prevent data races: store/update settings via atomic.Value (copy-on-write) or protect all mutations of LegalSettings with a dedicated mutex.

🤖 Prompt for AI Agents
In setting/system_setting/legal.go around lines 1 to 21, the file registers and
returns a pointer to defaultLegalSettings which permits callers to mutate the
struct unsafely; refactor so mutations are guarded: replace the package-level
pointer with an atomic.Value holding a LegalSettings value (or add a package
mutex) and update Register/Get to use copy-on-write semantics — store the value
in atomic.Value, implement GetLegalSettings to return a copy (not the internal
address), and ensure any update path swaps a new value into the atomic.Value (or
acquire the mutex for writes) so concurrent reads are safe.

Comment on lines +19 to +21
func GetLegalSettings() *LegalSettings {
return &defaultLegalSettings
}

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

Race condition: returning pointer to mutable global.

GetLegalSettings() returns a pointer to the package-level defaultLegalSettings singleton. Concurrent access from multiple goroutines (e.g., simultaneous API calls to /api/user-agreement and admin updates via /api/option/) can cause data races, as Go's config update and HTTP handlers run concurrently without synchronization.

Apply this diff to return a defensive copy:

 func GetLegalSettings() *LegalSettings {
-	return &defaultLegalSettings
+	settings := defaultLegalSettings
+	return &settings
 }

If callers need to update these settings, introduce a setter with proper locking (e.g., sync.RWMutex) or ensure the config manager handles concurrent access safely.

🤖 Prompt for AI Agents
In setting/system_setting/legal.go around lines 19-21, GetLegalSettings
currently returns a pointer to the package-level defaultLegalSettings which
causes a race when callers mutate it; change GetLegalSettings to return a
defensive copy (create a new LegalSettings value, copy fields from
defaultLegalSettings, and return a pointer to that new value) so callers cannot
mutate the global; if callers must update global settings add a setter function
and protect reads/writes with a sync.RWMutex (use RLock/RUnlock in getters and
Lock/Unlock in the setter) or ensure updates go through the existing config
manager with proper synchronization.

Comment on lines +51 to +66
const sanitizeHtml = (html) => {
// 创建一个临时元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;

// 提取样式
const styles = Array.from(tempDiv.querySelectorAll('style'))
.map(style => style.innerHTML)
.join('\n');

// 提取body内容,如果没有body标签则使用全部内容
const bodyContent = tempDiv.querySelector('body');
const content = bodyContent ? bodyContent.innerHTML : html;

return { content, styles };
};

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

Critical: Inadequate HTML sanitization exposes XSS vulnerability.

The sanitizeHtml function does not actually sanitize HTML content—it only extracts and returns raw HTML without removing dangerous scripts, event handlers, or malicious attributes. This creates an XSS vulnerability, especially if admin credentials are compromised.

Recommended fix: Use a proper HTML sanitization library like DOMPurify:

+import DOMPurify from 'dompurify';
+
 // 安全地渲染HTML内容
 const sanitizeHtml = (html) => {
-  // 创建一个临时元素来解析HTML
-  const tempDiv = document.createElement('div');
-  tempDiv.innerHTML = html;
-  
-  // 提取样式
-  const styles = Array.from(tempDiv.querySelectorAll('style'))
-    .map(style => style.innerHTML)
-    .join('\n');
-  
-  // 提取body内容,如果没有body标签则使用全部内容
-  const bodyContent = tempDiv.querySelector('body');
-  const content = bodyContent ? bodyContent.innerHTML : html;
-  
-  return { content, styles };
+  // 使用 DOMPurify 进行安全的 HTML 清理
+  const cleanHtml = DOMPurify.sanitize(html, {
+    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 
+                    'ul', 'ol', 'li', 'a', 'div', 'span', 'style'],
+    ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'style'],
+    ALLOW_DATA_ATTR: false,
+  });
+  
+  // 提取样式和内容
+  const tempDiv = document.createElement('div');
+  tempDiv.innerHTML = cleanHtml;
+  
+  const styles = Array.from(tempDiv.querySelectorAll('style'))
+    .map(style => style.textContent)
+    .join('\n');
+  
+  const bodyContent = tempDiv.querySelector('body');
+  const content = bodyContent ? bodyContent.innerHTML : cleanHtml;
+  
+  return { content, styles };
 };

Install DOMPurify:

npm install dompurify
npm install --save-dev @types/dompurify  # if using TypeScript
🤖 Prompt for AI Agents
In web/src/components/common/DocumentRenderer/index.jsx around lines 51 to 66,
the current sanitizeHtml function only extracts raw HTML and styles and does not
remove scripts, event handlers, or dangerous attributes, creating an XSS risk;
replace this custom extractor with DOMPurify: install dompurify, import it, use
DOMPurify.sanitize on the incoming HTML with a strict config (for example
allowing only needed tags and attributes and forbidding all event handlers and
scripts), separately extract and sanitize style tags if you must keep inline
CSS, and return the sanitized content and sanitized styles; ensure you do not
use innerHTML with unsanitized input anywhere in this file.

Comment on lines +207 to +211
useEffect(() => {
if (styles && styles !== htmlStyles) {
setHtmlStyles(styles);
}
}, [content, styles, htmlStyles]);

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

Critical: React hooks rule violation.

The useEffect hook at lines 207-211 is called conditionally inside the if (isHtmlContent(content)) render block. This violates React's Rules of Hooks, which require hooks to be called unconditionally at the component's top level.

Impact: This can cause:

  • State corruption and unpredictable behavior
  • React's internal hook tracking to break
  • Potential runtime errors during re-renders

Recommended fix: The HTML style injection logic is already handled by the useEffect at lines 130-151. Remove this duplicate conditional hook:

   // 如果是 HTML 内容,直接渲染
   if (isHtmlContent(content)) {
-    const { content: htmlContent, styles } = sanitizeHtml(content);
-    
-    // 设置样式(如果有的话)
-    useEffect(() => {
-      if (styles && styles !== htmlStyles) {
-        setHtmlStyles(styles);
-      }
-    }, [content, styles, htmlStyles]);
-    
     return (
       <div className='min-h-screen bg-gray-50'>
         <div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
           <div className='bg-white rounded-lg shadow-sm p-8'>
             <Title heading={2} className='text-center mb-8'>{title}</Title>
             <div 
               className='prose prose-lg max-w-none'
-              dangerouslySetInnerHTML={{ __html: htmlContent }}
+              dangerouslySetInnerHTML={{ __html: processedHtmlContent }}
             />
           </div>
         </div>
       </div>
     );
   }

The existing processContent function and useEffect at lines 130-151 already handle style extraction and injection correctly.

📝 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
useEffect(() => {
if (styles && styles !== htmlStyles) {
setHtmlStyles(styles);
}
}, [content, styles, htmlStyles]);
// 如果是 HTML 内容,直接渲染
if (isHtmlContent(content)) {
return (
<div className='min-h-screen bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>{title}</Title>
<div
className='prose prose-lg max-w-none'
dangerouslySetInnerHTML={{ __html: processedHtmlContent }}
/>
</div>
</div>
</div>
);
}
🤖 Prompt for AI Agents
In web/src/components/common/DocumentRenderer/index.jsx around lines 207 to 211,
there is a duplicate useEffect that is conditionally executed inside the
isHtmlContent render block which violates React's Rules of Hooks; remove this
conditional useEffect block entirely (the HTML style extraction/injection is
already handled by the useEffect at lines 130-151) and ensure any style-setting
logic runs only from that top-level effect or a helper called unconditionally
from the component body so no hooks are invoked inside conditional render
branches.

Comment on lines +378 to +411
<Form.TextArea
label={t('用户协议')}
placeholder={t(
'在此输入用户协议内容,支持 Markdown & HTML 代码',
)}
field={LEGAL_USER_AGREEMENT_KEY}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议')}
/>
<Button
onClick={submitUserAgreement}
loading={loadingInput[LEGAL_USER_AGREEMENT_KEY]}
>
{t('设置用户协议')}
</Button>
<Form.TextArea
label={t('隐私政策')}
placeholder={t(
'在此输入隐私政策内容,支持 Markdown & HTML 代码',
)}
field={LEGAL_PRIVACY_POLICY_KEY}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策')}
/>
<Button
onClick={submitPrivacyPolicy}
loading={loadingInput[LEGAL_PRIVACY_POLICY_KEY]}
>
{t('设置隐私政策')}
</Button>

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

❓ Verification inconclusive

Verify registration flow enforces consent gating.

The helpText on lines 387 and 404 states that users will be required to check agreement/policy during registration. Ensure that RegisterForm.jsx actually implements this gating logic when these settings are non-empty.

Run the following script to check if the registration form references legal settings:


🏁 Script executed:

#!/bin/bash
# Search for user agreement and privacy policy references in registration
rg -nP --type=js --type=jsx -C5 'user.*agreement|privacy.*policy' web/src/components/auth/RegisterForm.jsx

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for legal settings keys in the registration form
rg -n 'LEGAL_USER_AGREEMENT_KEY|LEGAL_PRIVACY_POLICY_KEY' -C5 web/src/components/auth/RegisterForm.jsx

Length of output: 0


Enforce consent gating in web/src/components/auth/RegisterForm.jsx.
No references to LEGAL_USER_AGREEMENT_KEY or LEGAL_PRIVACY_POLICY_KEY were found; require users to accept both before allowing registration.

🤖 Prompt for AI Agents
web/src/components/settings/OtherSetting.jsx lines 378-411, and update
web/src/components/auth/RegisterForm.jsx: The settings UI defines
LEGAL_USER_AGREEMENT_KEY and LEGAL_PRIVACY_POLICY_KEY but registration does not
enforce consent; update RegisterForm.jsx to require explicit acceptance of both
before allowing registration. Load the two legal settings at component mount (or
from context/store), render two checkboxes ("I have read and agree to the User
Agreement" and "I have read and agree to the Privacy Policy") with links to the
respective content, disable the submit button until both boxes are checked, and
add validation on submit to block the request and show a user-facing error if
either consent is missing; ensure the checkboxes' state is included in any form
payload or client-side validation and that server-side validation is also
present if applicable.

@Calcium-Ion
Calcium-Ion merged commit 0c18139 into QuantumNous:main Oct 10, 2025
1 check passed
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
feat(web): add settings & pages of privacy policy & user agreement
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.

3 participants