Skip to content

feat: 新增邀请开关与分组管理,并修复权限同步及UI样式问题 - #1703

Closed
x-Ai wants to merge 18 commits into
QuantumNous:alphafrom
x-Ai:alpha
Closed

feat: 新增邀请开关与分组管理,并修复权限同步及UI样式问题#1703
x-Ai wants to merge 18 commits into
QuantumNous:alphafrom
x-Ai:alpha

Conversation

@x-Ai

@x-Ai x-Ai commented Aug 31, 2025

Copy link
Copy Markdown
Contributor
  1. 新增"邀请功能"开关
  2. 新增"分组管理"按扭(分组及倍率自动同步至"支付设置"和"倍率设置"中)
  3. 新增功能均有完整的鉴权校验
  4. 新增功能均i18n本地化
  5. 修复侧边栏权限同步问题
  6. 修复分组权限同步问题
  7. 修复主页、登录页面样式错位问题,统一风格
  8. 修复边栏设置设置后界面不能立即生效的问题
  9. 修复其他用户体验的小问题
  10. 修复一些基于coderabbitai审查的的代码问题
  11. 现在可以自定义头像
1 2 3 4 5 6 avatar

Summary by CodeRabbit

  • New Features

    • Role-aware module visibility & per-module route guarding; dedicated avatar endpoint; avatar upload/preview/update with client-side caching; user groups CRUD + management UI; system-driven sidebar_config with instant refresh.
  • UI/UX

    • Shared auth page layout; avatar upload in profile header; conditional invitation card and gating; responsive flex/layout and footer/header refinements; group management modals and sidebar settings overhaul.
  • i18n

    • Many new/updated translations (including “Guest”) for groups, avatars, invitations, docs, and admin UI.
  • Other

    • System name/title sync across tabs; invitation feature can be globally disabled; safer top-up ratio access.

@coderabbitai

coderabbitai Bot commented Aug 31, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds role-aware filtering for header and admin sidebar modules, an invitation feature toggle, avatar endpoints with client caching and validation, a UserGroup domain (model + CRUD + UI), module-level auth middleware and client ModuleRoute guards, and a unified sidebarConfig with cross-instance refresh; many UI/i18n/layout adjustments accompany these changes.

Changes

Cohort / File(s) Summary
Status & Invitation
controller/misc.go, setting/operation_setting/general_setting.go, web/src/pages/Setting/Operation/SettingsGeneral.jsx, web/src/components/topup/index.jsx
Add invitation_enabled to status payload and GeneralSetting; expose flag in UI; gate invite flows and TopUp UI on setting.
Header / Sidebar Filtering (backend)
controller/misc.go, middleware/auth.go, controller/pricing.go
Filter header_nav_modules and SidebarModulesAdmin per-request by user role; add ModuleAuth(modulePath) middleware and nested permission checks; gate pricing endpoint by header nav pricing config and optional auth.
User & Avatar (backend)
controller/user.go, model/user.go, router/api-router.go
Add Avatar field to User, GetUserAvatar endpoint, avatar validation/update paths; change GetSelf to return sidebar_config and permissions rather than raw avatar; include avatar route in router.
UserGroup Model, Controller & Migration (backend)
model/user_group.go, controller/user_group.go, model/main.go, router/api-router.go
Introduce UserGroup model and seeding, CRUD controller, /user_group admin routes, include UserGroup in migrations and fast migrate path.
Sidebar Config & Client Guards (frontend)
web/src/hooks/common/useSidebar.js, web/src/components/auth/ModuleRoute.jsx, web/src/App.jsx
Unify admin/user configs into sidebarConfig, add global sidebar-refresh event, implement ModuleRoute guard, and wrap routes with per-module modulePath.
UserGroup UI & Integrations (frontend)
web/src/components/table/users/*, web/src/components/table/users/modals/*, web/src/helpers/api.js, web/src/helpers/render.jsx, web/src/i18n/locales/en.json, web/src/components/table/users/UsersActions.jsx, web/src/components/table/users/index.jsx
Add UserGroup management UI (SideSheet, modals), integrate group select in AddUser, translate default groups, and wire refresh hooks/props.
Avatar Cache & Manager (frontend)
web/src/helpers/avatarCache.js, web/src/helpers/userDataManager.js, web/src/context/User/reducer.js, web/src/components/settings/personal/components/UserInfoHeader.jsx, web/src/pages/Setting/Personal/PersonalSetting.jsx, web/src/hooks/dashboard/useDashboardData.js
Add local avatar cache utilities, userDataManager (get/update/cleanup), logout cache cleanup, avatar upload UI with preview/validation, and consumers updated to use helper + refresh flows.
Settings UIs: Sidebar & Header (frontend)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx, web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx, web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx
Admin sidebar gains nested admin.user.{enabled,groupManagement}; personal sidebar rewritten to merge system/user configs; header nav persisted as header_nav_modules.
Auth Layout & Pages (frontend)
web/src/components/auth/AuthPageLayout.jsx, web/src/components/auth/LoginForm.jsx, web/src/components/auth/RegisterForm.jsx, web/src/components/auth/PasswordReset*.jsx
Add shared AuthPageLayout, migrate auth pages to use it, remove decorative blur elements, and standardize inner sizing/padding.
Pricing gating & client redirects
controller/pricing.go, web/src/hooks/model-pricing/useModelPricingData.jsx
Server-side pricing access checks; client hook handles 403 by redirecting to /login or /forbidden.
Utilities, Hooks & Minor UI tweaks (frontend)
web/src/helpers/utils.jsx, web/src/hooks/common/useUserPermissions.js, web/src/components/settings/personal/cards/NotificationSettings.jsx, web/src/components/layout/Footer.jsx, web/src/pages/Home/index.jsx, web/src/components/layout/PageLayout.jsx, web/index.html, web/src/helpers/data.js, web/src/components/layout/headerbar/LanguageSelector.jsx, web/src/constants/user.constants.js, web/src/hooks/common/useHeaderBar.js
compareObjects now unions keys; muted dev logs; notification saves trigger sidebar refresh; footer border and layout flex tweaks; document.title sync + CustomEvent; system_name normalization + event dispatch; language label tweak; role constants/helpers; header hook reads header_nav_modules.
Topup / Group Ratio Concurrency (backend)
common/topup-ratio.go
Add RWMutex protection and GetTopupGroupRatioCopy() for thread-safe TopupGroupRatio access.
Minor backend formatting & invitation gating
model/option.go, model/main.go, model/user.go, controller/user.go
Formatting tweaks; include UserGroup in migrations and init; export default sidebar generator; gate invitation flows on setting; add UpdateUsersGroupName and UpdateAvatar helpers.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as /api/status
  participant Server as GetStatus
  Note over Client,Server: Client requests status (TopUp/UI)
  Client->>API: GET /api/status
  API->>Server: GetStatus reads role, invitation flag, filters header_nav_modules & sidebar_config
  Server-->>API: { invitation_enabled, header_nav_modules, sidebar_config, ... }
  API-->>Client: status payload
  alt invitation_enabled == true
    Client->>API: GET /api/user/aff/link
    API-->>Client: { success, link }
  end
Loading
sequenceDiagram
  autonumber
  actor Browser
  participant HookA as useSidebar (instance A)
  participant Bus as window.sidebarEventTarget
  participant HookB as useSidebar (instance B)
  Browser->>HookA: save sidebar settings
  HookA->>HookA: loadSidebarConfig()
  HookA-->>Bus: dispatch "sidebar-refresh"
  Bus-->>HookB: "sidebar-refresh"
  HookB->>HookB: loadSidebarConfig()
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant UI as UserGroupManagement
  participant API as /api/user_group
  participant Model as model.UserGroup
  Admin->>UI: Create/Update/Delete group
  UI->>API: POST/PUT/DELETE /api/user_group
  API->>Model: Insert/Update/Delete
  Model-->>API: result
  API->>API: sync to GroupRatio/UserUsableGroups/Topup (logs errors)
  API-->>UI: success
Loading
sequenceDiagram
  autonumber
  actor Client
  participant Manager as userDataManager
  participant API as /api/user/self
  participant AvatarAPI as /api/user/avatar
  Client->>Manager: getUserData()
  Manager->>API: GET /api/user/self
  API-->>Manager: user data (no binary)
  alt cached avatar exists
    Manager->>Manager: load avatar from local cache
  else
    Manager->>AvatarAPI: GET /api/user/avatar
    AvatarAPI-->>Manager: avatar data
    Manager->>Manager: cache avatar locally
  end
  Manager-->>Client: combined user data with avatar
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately summarizes the primary changes: adding an invitation toggle and group management, and fixing permission synchronization and UI styling. The PR details and raw_summary show matching work across backend (invitation_enabled, user group model/controllers, middleware) and frontend (group management UI, sidebar and layout fixes), so the title reflects the main scope. It is concise, specific, and suitable as a single-line changelog entry for reviewers scanning history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

"I hop through keys with careful cheer,
Sidebars trimmed and invites appear.
Avatars cached beneath my paw,
Groups arranged — I tweak the law.
A rabbit codes — tests green, deploy near." 🐇✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b2539a and 6d4d815.

📒 Files selected for processing (1)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/table/users/modals/UserGroupManagement.jsx
✨ 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

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: 2

Caution

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

⚠️ Outside diff range comments (1)
controller/user.go (1)

526-586: Remove unused generateDefaultSidebarConfig function

This function is defined in controller/user.go but never referenced; safe to delete to avoid code drift.

-controller/user.go
-func generateDefaultSidebarConfig(userRole int) string { ... }
🧹 Nitpick comments (13)
model/option.go (1)

158-159: Whitespace-only change — safe to keep or drop

No functional impact. Consider avoiding formatting-only diffs to keep history clean.

model/main.go (1)

133-134: Formatting-only change

No behavioral change. Fine to keep; alternatively, drop the extra blank lines for consistency.

web/src/hooks/common/useUserPermissions.js (1)

22-30: Prefer env-gated debug logs over commented-out statements

Commented logs tend to linger. If you want them available in dev, gate with NODE_ENV and use console.debug/warn.

Apply this diff:

-        //console.log('用户权限加载成功:', userPermissions);
+        if (process.env.NODE_ENV !== 'production') {
+          console.debug('用户权限加载成功:', userPermissions);
+        }
...
-        //console.error('获取权限失败:', res.data.message);
+        if (process.env.NODE_ENV !== 'production') {
+          console.warn('获取权限失败:', res.data.message);
+        }
...
-      //console.error('加载用户权限异常:', error);
+      if (process.env.NODE_ENV !== 'production') {
+        console.error('加载用户权限异常:', error);
+      }
web/src/helpers/utils.jsx (1)

297-307: Harden compareObjects against nulls and improve equality (treat NaN as equal; detect added/removed undefined).

Guard against null/undefined inputs and use Object.is; also mark keys added/removed when the value is undefined.

Apply this diff:

-  // 获取两个对象的所有键
-  const allKeys = new Set([...Object.keys(oldObject), ...Object.keys(newObject)]);
+  // 获取两个对象的所有键(空值保护)
+  const oldKeys = oldObject && typeof oldObject === 'object' ? Object.keys(oldObject) : [];
+  const newKeys = newObject && typeof newObject === 'object' ? Object.keys(newObject) : [];
+  const allKeys = new Set([...oldKeys, ...newKeys]);

-  // 比较所有键的值
-  for (const key of allKeys) {
-    if (oldObject[key] !== newObject[key]) {
-      changedProperties.push({
-        key: key,
-        oldValue: oldObject[key],
-        newValue: newObject[key],
-      });
-    }
-  }
+  // 比较所有键的值(使用 Object.is;区分“缺失”与“undefined”)
+  for (const key of allKeys) {
+    const oldHas = oldObject && Object.prototype.hasOwnProperty.call(oldObject, key);
+    const newHas = newObject && Object.prototype.hasOwnProperty.call(newObject, key);
+    const oldVal = oldHas ? oldObject[key] : undefined;
+    const newVal = newHas ? newObject[key] : undefined;
+    if (!Object.is(oldVal, newVal) || oldHas !== newHas) {
+      changedProperties.push({
+        key,
+        oldValue: oldHas ? oldVal : undefined,
+        newValue: newHas ? newVal : undefined,
+      });
+    }
+  }
web/src/components/topup/index.jsx (1)

328-334: Avoid duplicate status fetch; prefer StatusContext signal when available.

Leverage statusState.status.invitation_enabled to set flags, and only call getInvitationConfig if it’s undefined (see snippet above).

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

2079-2081: Typo in Chinese key: “不在启用” → “不再启用”.

To avoid propagating a typo in key IDs, add a corrected alias key and migrate callers gradually.

Apply this minimal addition near the same block:

   "邀请功能": "Invitation Feature",
-  "关闭后:不在启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled"
+  "关闭后:不在启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled",
+  "关闭后:不再启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled"

Follow-up: update UI code to use the corrected key, then remove the old one in a later cleanup.

model/user.go (1)

404-411: Avoid re-query by username; read by ID (already available) to reduce race risk.

Querying by username after insert is unnecessary and could be brittle if username changes. Prefer ID.

Apply this diff:

-  var createdUser User
-  if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil {
+  var createdUser User
+  if err := DB.First(&createdUser, user.Id).Error; err == nil {
     // 生成基于角色的默认边栏配置
     defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(createdUser.Role)
     if defaultSidebarConfig != "" {
       currentSetting := createdUser.GetSetting()
       currentSetting.SidebarModules = defaultSidebarConfig
       createdUser.SetSetting(currentSetting)
       createdUser.Update(false)
       common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
     }
   }
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)

93-111: 合并逻辑可读性与健壮性可再加强(避免多次 Object.keys 与更宽松布尔解析)

  • 预先缓存 keys,避免循环中重复 Object.keys(inputs).
  • 布尔解析可容错 'TRUE'/'False'/1/0' 等。

应用如下局部改动:

-    // 从初始 inputs 开始,确保保留所有默认值
-    const currentInputs = { ...inputs };
-
-    // 用 props.options 中的值覆盖对应的键
-    for (let key in props.options) {
-      if (Object.keys(inputs).includes(key)) {
-        let value = props.options[key];
-        // 对于布尔类型的字段,需要进行字符串到布尔值的转换
-        if (typeof inputs[key] === 'boolean') {
-          if (typeof value === 'string') {
-            value = value === 'true';
-          } else if (typeof value === 'boolean') {
-            value = value;
-          } else {
-            value = inputs[key]; // 保持默认值
-          }
-        }
-        currentInputs[key] = value;
-      }
-    }
+    // 从初始 inputs 开始,确保保留所有默认值
+    const inputKeys = new Set(Object.keys(inputs));
+    const currentInputs = { ...inputs };
+
+    // 用 props.options 中的值覆盖对应的键
+    for (const [key, raw] of Object.entries(props.options || {})) {
+      if (!inputKeys.has(key)) continue;
+      let value = raw;
+      // 布尔字段:支持多种字符串/数值表示
+      if (typeof inputs[key] === 'boolean') {
+        if (typeof raw === 'boolean') value = raw;
+        else value = ['true', '1', 'yes', 'on'].includes(String(raw).toLowerCase());
+      }
+      currentInputs[key] = value;
+    }

注意:当前非布尔字段会以字符串形式保存到后端。如果后端对数值键(如 RetryTimesUSDExchangeRate)要求为数字,请确认 API 契约,避免类型不匹配。

controller/user.go (5)

379-388: 邀请功能关闭时的前置校验合理

在额度划转前进行特性门控,避免无效业务执行。建议同时记录一次系统日志以便审计。

可在返回前添加:

common.SysLog("拒绝访问 TransferAffQuota:邀请功能已禁用")

415-424: 邀请码获取的特性门控一致性良好

同样建议增加一条系统日志,便于排查被拦截的请求。

common.SysLog("拒绝访问 GetAffCode:邀请功能已禁用")

499-524: 权限结构建议使用强类型 DTO

当前返回 map[string]interface{},可读性与可维护性一般。建议定义 DTO 结构体,便于后续演进与 swagger 文档化。

如:

type SidebarPermissions struct {
  SidebarSettings bool                   `json:"sidebar_settings"`
  SidebarModules  map[string]interface{} `json:"sidebar_modules"`
}

976-983: 升级为管理员后的边栏同步:日志时机建议放在持久化成功之后

当前在 user.Update(false) 之前记录成功日志,若后续更新失败会产生误导。

可将日志延后到 Update 成功后或调整文案为“已尝试同步更新边栏配置”。


1001-1008: 降级为普通用户后的边栏同步:同上,建议调整日志时机/文案

与升级路径保持一致,避免误导。

📜 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 5cfc133 and 8cc5763.

📒 Files selected for processing (12)
  • controller/misc.go (1 hunks)
  • controller/user.go (7 hunks)
  • model/main.go (1 hunks)
  • model/option.go (1 hunks)
  • model/user.go (4 hunks)
  • setting/operation_setting/general_setting.go (1 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (1 hunks)
  • web/src/components/topup/index.jsx (6 hunks)
  • web/src/helpers/utils.jsx (1 hunks)
  • web/src/hooks/common/useUserPermissions.js (1 hunks)
  • web/src/i18n/locales/en.json (2 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
controller/misc.go (1)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
web/src/components/topup/index.jsx (2)
web/src/components/topup/InvitationCard.jsx (1)
  • InvitationCard (34-227)
web/src/helpers/render.jsx (1)
  • renderQuota (899-917)
model/user.go (4)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/constants.go (2)
  • QuotaForInvitee (100-100)
  • QuotaForInviter (99-99)
model/log.go (1)
  • RecordLog (78-94)
logger/logger.go (1)
  • LogQuota (94-100)
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)
web/src/helpers/render.jsx (1)
  • key (426-426)
controller/user.go (3)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
model/user.go (1)
  • GenerateDefaultSidebarConfigForRole (96-155)
common/sys_log.go (1)
  • SysLog (10-13)
🔇 Additional comments (12)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)

561-561: Wording tweak LGTM

Improved Chinese phrasing reads better and matches i18n usage.

setting/operation_setting/general_setting.go (1)

9-10: Approve invitation feature flag wiring

All end-to-end wiring confirmed: registered via config.GlobalConfig.Register("general_setting", &generalSetting), exposed in status API, gated in TransferAffQuota/GetAffCode, and UI toggle present in SettingsGeneral.jsx (with default true). Dynamic updates handled by the global config registration.

web/src/components/topup/index.jsx (2)

63-65: Good: pre-init invitation flags to avoid flicker.


519-567: UI gating looks correct and prevents layout shift.

Conditional grid and right column render only when config is loaded and enabled. Nicely done.

controller/misc.go (1)

70-71: Expose invitation_enabled in status — LGTM.

Consistent with operation settings and frontend gating.

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

2023-2023: Add “系统主页” → “System homepage” — OK.


2074-2074: Wording fix looks good.

“You can customize the sidebar functions to display” reads better than the previous phrasing.

model/user.go (2)

96-156: Exported default sidebar generator — OK.

Clear role-based defaults; JSON shape is reasonable.


418-430: Invitation gating in Insert — LGTM. SidebarModules is defined as a string in dto/user_settings.go and used consistently across frontend and backend, so serialization matches expectations.

web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)

46-46: 默认值与后端一致,OK

默认开启邀请功能与后端默认值对齐。

controller/user.go (2)

13-13: 新增依赖 OK

引入 operation_setting 以读取全局开关,符合本次特性需求。


468-488: GetSelf 返回字段扩展 OK,但请确认前端契约

  • sidebar_modules 为字符串(JSON),permissions.sidebar_modules 为对象,前端需分别处理。

请确认前端消费 GetSelf 的位置(例如侧边栏与设置页)已适配上述两个不同的字段类型,避免解析错误或 UI 逻辑分歧。

Comment thread web/src/components/topup/index.jsx
Comment thread web/src/pages/Setting/Operation/SettingsGeneral.jsx
@x-Ai x-Ai changed the title 新增"邀请功能"开关 新增"邀请"功能开关、"分组管理"按扭 Aug 31, 2025

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

Caution

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

⚠️ Outside diff range comments (4)
web/src/components/auth/PasswordResetConfirm.jsx (1)

84-105: Add try/catch/finally and prevent default to avoid stuck loading and unhandled rejections

Network errors currently leave loading=true/disableButton=true and can crash silently.

-  async function handleSubmit(e) {
-    if (!email || !token) {
+  async function handleSubmit(e) {
+    e?.preventDefault();
+    if (!email || !token) {
       showError(t('无效的重置链接,请重新发起密码重置请求'));
       return;
     }
     setDisableButton(true);
     setLoading(true);
-    const res = await API.post(`/api/user/reset`, {
-      email,
-      token,
-    });
-    const { success, message } = res.data;
-    if (success) {
-      let password = res.data.data;
-      setNewPassword(password);
-      await copy(password);
-      showNotice(`${t('密码已重置并已复制到剪贴板:')} ${password}`);
-    } else {
-      showError(message);
-    }
-    setLoading(false);
+    try {
+      const res = await API.post(`/api/user/reset`, { email, token });
+      const { success, message, data } = res.data;
+      if (success) {
+        const password = data;
+        setNewPassword(password);
+        await copy(password);
+        showNotice(t('密码已重置并已复制到剪贴板'));
+      } else {
+        showError(message);
+      }
+    } catch (err) {
+      showError(t('网络错误,请稍后重试'));
+    } finally {
+      setLoading(false);
+    }
   }
web/src/components/auth/PasswordResetForm.jsx (1)

83-105: Guard API call with try/catch/finally and prevent default

Same error-handling gap as confirm page; failures leave UI stuck.

-  async function handleSubmit(e) {
+  async function handleSubmit(e) {
+    e?.preventDefault();
     if (!email) {
       showError(t('请输入邮箱地址'));
       return;
     }
     if (turnstileEnabled && turnstileToken === '') {
       showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
       return;
     }
     setDisableButton(true);
     setLoading(true);
-    const res = await API.get(
-      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
-    );
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess(t('重置邮件发送成功,请检查邮箱!'));
-      setInputs({ ...inputs, email: '' });
-    } else {
-      showError(message);
-    }
-    setLoading(false);
+    try {
+      const res = await API.get(
+        `/api/reset_password?email=${encodeURIComponent(email)}&turnstile=${turnstileToken}`,
+      );
+      const { success, message } = res.data;
+      if (success) {
+        showSuccess(t('重置邮件发送成功,请检查邮箱!'));
+        setInputs({ ...inputs, email: '' });
+      } else {
+        showError(message);
+      }
+    } catch (err) {
+      showError(t('网络错误,请稍后重试'));
+    } finally {
+      setLoading(false);
+    }
   }
model/user.go (1)

399-413: Avoid re-query and handle update error.

After DB.Create, GORM populates user.Id. Re-querying by username is unnecessary and can race; also the Update error is ignored.

-  var createdUser User
-  if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil {
-    // 生成基于角色的默认边栏配置
-    defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(createdUser.Role)
-    if defaultSidebarConfig != "" {
-      currentSetting := createdUser.GetSetting()
-      currentSetting.SidebarModules = defaultSidebarConfig
-      createdUser.SetSetting(currentSetting)
-      createdUser.Update(false)
-      common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
-    }
-  }
+  // 直接使用已创建的用户,避免额外查询
+  createdUser := *user
+  defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(createdUser.Role)
+  if defaultSidebarConfig != "" {
+    currentSetting := createdUser.GetSetting()
+    currentSetting.SidebarModules = defaultSidebarConfig
+    createdUser.SetSetting(currentSetting)
+    if err := createdUser.Update(false); err != nil {
+      common.SysLog("初始化边栏配置失败: " + err.Error())
+    } else {
+      common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
+    }
+  }
web/src/components/table/users/modals/AddUserModal.jsx (1)

73-86: Fix: handle network errors in submit to avoid stuck spinner

Wrap the POST in try/catch/finally; otherwise a thrown Axios error leaves loading=true and no feedback.

 const submit = async (values) => {
   setLoading(true);
-  const res = await API.post(`/api/user/`, values);
-  const { success, message } = res.data;
-  if (success) {
-    showSuccess(t('用户账户创建成功!'));
-    formApiRef.current?.setValues(getInitValues());
-    props.refresh();
-    props.handleClose();
-  } else {
-    showError(message);
-  }
-  setLoading(false);
+  try {
+    const res = await API.post('/api/user/', values);
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess(t('用户账户创建成功!'));
+      formApiRef.current?.setValues(getInitValues());
+      props.refresh?.();
+      props.handleClose();
+    } else {
+      showError(message);
+    }
+  } catch (error) {
+    showError(error);
+  } finally {
+    setLoading(false);
+  }
 };
🧹 Nitpick comments (37)
web/src/components/auth/AuthPageLayout.jsx (3)

24-24: Use min-h-screen and add dark background

Current min-h-full may not fill viewport; also no dark mode background. Recommend:

-    <div className='relative overflow-hidden bg-gray-100 min-h-full flex items-center justify-center pt-16 pb-8'>
+    <div className='relative overflow-hidden bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center pt-16 pb-8'>

26-33: Mark decorative elements aria-hidden

Avoid announcing purely decorative nodes.

-      <div
-        className='blur-ball blur-ball-indigo'
+      <div
+        aria-hidden='true'
+        className='blur-ball blur-ball-indigo'
         style={{ top: '-80px', right: '-80px', transform: 'none' }}
       />
-      <div
-        className='blur-ball blur-ball-teal'
+      <div
+        aria-hidden='true'
+        className='blur-ball blur-ball-teal'
         style={{ top: '50%', left: '-120px' }}
       />

22-22: Add PropTypes for children

Lightweight runtime contract for JSX consumers.

-import React from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';

And below the component:

 };
 
+AuthPageLayout.propTypes = {
+  children: PropTypes.node,
+};
web/src/components/auth/PasswordResetConfirm.jsx (3)

134-141: Prefer form onSubmit over button onClick; ensure no double submit

Use Form onSubmit and let the primary button submit the form.

-                <Form
-                  getFormApi={(api) => setFormApi(api)}
+                <Form
+                  getFormApi={(api) => setFormApi(api)}
+                  onSubmit={handleSubmit}
                   initValues={{
                     email: email || '',
                     newPassword: newPassword || '',
                   }}
                   className='space-y-4'
                 >
...
-                    <Button
+                    <Button
                       theme='solid'
                       className='w-full !rounded-full'
                       type='primary'
-                      htmlType='submit'
-                      onClick={handleSubmit}
+                      htmlType='submit'
                       loading={loading}
                       disabled={
                         disableButton || newPassword || !isValidResetLink
                       }
                     >

Also applies to: 176-189


44-45: Make isValidResetLink explicitly boolean

Avoid relying on truthy strings.

-  const isValidResetLink = email && token;
+  const isValidResetLink = Boolean(email && token);

71-83: Optimize countdown interval to avoid re-creating timer every tick

Use functional state update and static interval.

-  useEffect(() => {
-    let countdownInterval = null;
-    if (disableButton && countdown > 0) {
-      countdownInterval = setInterval(() => {
-        setCountdown(countdown - 1);
-      }, 1000);
-    } else if (countdown === 0) {
-      setDisableButton(false);
-      setCountdown(30);
-    }
-    return () => clearInterval(countdownInterval);
-  }, [disableButton, countdown]);
+  useEffect(() => {
+    if (!disableButton) return;
+    const id = setInterval(() => {
+      setCountdown((c) => {
+        if (c <= 1) {
+          clearInterval(id);
+          setDisableButton(false);
+          return 30;
+        }
+        return c - 1;
+      });
+    }, 1000);
+    return () => clearInterval(id);
+  }, [disableButton]);
web/src/components/auth/PasswordResetForm.jsx (3)

126-152: Handle submit on the Form; avoid duplicate handlers

Let the form own submission; keep button as submit.

-                <Form className='space-y-3'>
+                <Form className='space-y-3' onSubmit={handleSubmit}>
...
-                    <Button
+                    <Button
                       theme='solid'
                       className='w-full !rounded-full'
                       type='primary'
                       htmlType='submit'
-                      onClick={handleSubmit}
                       loading={loading}
                       disabled={disableButton}
                     >

Also applies to: 137-151


55-64: Defensive parse of localStorage status

Invalid JSON will throw and break render.

-  useEffect(() => {
-    let status = localStorage.getItem('status');
-    if (status) {
-      status = JSON.parse(status);
+  useEffect(() => {
+    let status = localStorage.getItem('status');
+    if (status) {
+      try {
+        status = JSON.parse(status);
+      } catch {
+        status = null;
+      }
       if (status.turnstile_check) {
         setTurnstileEnabled(true);
         setTurnstileSiteKey(status.turnstile_site_key);
       }
     }
   }, []);

66-78: Same countdown optimization as confirm page

Reduce effect churn.

-  useEffect(() => {
-    let countdownInterval = null;
-    if (disableButton && countdown > 0) {
-      countdownInterval = setInterval(() => {
-        setCountdown(countdown - 1);
-      }, 1000);
-    } else if (countdown === 0) {
-      setDisableButton(false);
-      setCountdown(30);
-    }
-    return () => clearInterval(countdownInterval);
-  }, [disableButton, countdown]);
+  useEffect(() => {
+    if (!disableButton) return;
+    const id = setInterval(() => {
+      setCountdown((c) => {
+        if (c <= 1) {
+          clearInterval(id);
+          setDisableButton(false);
+          return 30;
+        }
+        return c - 1;
+      });
+    }, 1000);
+    return () => clearInterval(id);
+  }, [disableButton]);
web/src/components/layout/Footer.jsx (1)

224-224: Prefer class for border; keep color via CSS var.

Inline style makes theming overrides and dedup harder. Use a utility class for width and keep the CSS var for color.

-    <div className='w-full' style={{ borderTop: '1px solid var(--semi-color-border)' }}>
+    <div className='w-full border-t' style={{ borderTopColor: 'var(--semi-color-border)' }}>
model/user.go (2)

95-155: Exporting GenerateDefaultSidebarConfigForRole: confirm external callers and JSON schema stability.

Since this is now exported, ensure all external uses expect a JSON string (not a struct). Consider documenting the schema to avoid drift.


418-430: Invitation gate OK; consider user feedback/log on disabled state.

Logic correctly no-ops when InvitationEnabled=false. Optionally record a system log once to aid ops diagnostics.

web/src/components/table/users/modals/EditUserGroupModal.jsx (1)

82-86: Auto-close modal on success (UX).

After success, also close the modal to match common patterns.

-      if (success) {
+      if (success) {
         showSuccess(isEdit ? t('分组更新成功!') : t('分组创建成功!'));
-        onSuccess();
+        onSuccess();
+        onClose?.();
       } else {
web/src/i18n/locales/en.json (2)

2074-2074: Improve phrasing for sidebar description.

More natural English.

-  "您可以个性化设置侧边栏要显示的功能": "You can customize the sidebar functions to display",
+  "您可以个性化设置侧边栏要显示的功能": "You can customize which sidebar functions are displayed",

2081-2081: Tighten wording for invitation toggle hint.

Crisper phrasing.

-  "关闭后:不在启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled",
+  "关闭后:不在启用邀请奖励功能": "When disabled: invitation rewards are disabled",
web/src/components/settings/personal/components/UserInfoHeader.jsx (1)

162-162: Deduplicate group display logic; use a small helper or shared util.

The ternary is duplicated and slightly hard to scan. Extract a tiny helper so both desktop/mobile use the same logic (and are easier to tweak later).

Apply this diff at the two occurrences:

- {userState?.user?.group === 'default' ? t('默认') : (userState?.user?.group || t('默认'))}
+ {getGroupDisplayName(userState?.user?.group)}

Add this local helper near the top of the component:

const getGroupDisplayName = (g) => (!g || g === 'default' ? t('默认') : g);

Or import a shared helper if you expose one from helpers/render (see my comment there).

Also applies to: 210-210

web/src/helpers/render.jsx (3)

632-639: Centralize getGroupDisplayName and export it for reuse.

Great to localize the default group label. To avoid duplicating this mapping across components/modals, promote this helper to a top-level exported function and reuse it here and elsewhere (e.g., UserInfoHeader, EditUserGroupModal, UserGroupManagement).

Apply this diff here to reuse a shared helper within this file:

-  // 获取分组显示名称
-  const getGroupDisplayName = (groupName) => {
-    if (groupName === 'default') {
-      return i18next.t('默认');
-    }
-    return groupName;
-  };
+  // 使用顶层导出的 getGroupDisplayName(见下方新增导出)

And add this top-level export (outside this function, e.g., after imports):

export const getGroupDisplayName = (groupName) =>
  !groupName || groupName === 'default' ? i18next.t('默认') : groupName;

No change needed to Line 661; it’ll keep calling getGroupDisplayName(group).

Also applies to: 661-661


644-652: Avoid shadowing the function parameter ‘group’.

Inside map, the iterator variable ‘group’ shadows the renderGroup parameter ‘group’, which can confuse readers and tooling. Rename the iterator.

-      {groups.map((group) => (
+      {groups.map((groupName) => (
         <Tag
-          color={tagColors[group] || stringToColor(group)}
-          key={group}
+          color={tagColors[groupName] || stringToColor(groupName)}
+          key={groupName}
           shape='circle'
           onClick={async (event) => {
             event.stopPropagation();
-            if (await copy(group)) {
-              showSuccess(i18next.t('已复制:') + group);
+            if (await copy(groupName)) {
+              showSuccess(i18next.t('已复制:') + groupName);
             } else {
               Modal.error({
                 title: i18next.t('无法复制到剪贴板,请手动复制'),
-                content: group,
+                content: groupName,
               });
             }
           }}
         >
-          {getGroupDisplayName(group)}
+          {getGroupDisplayName(groupName)}
         </Tag>
       ))}

646-647: Optional: neutral color for the “default” group for visual consistency.

Empty group renders with a white tag above; consider also using white for the “default” group to match that neutral style.

Example change (reference only):

color={groupName === 'default' ? 'white' : (tagColors[groupName] || stringToColor(groupName))}
model/main.go (1)

303-305: Seed defaults more robustly (idempotent per-name).

InitDefaultUserGroups() currently bails out when any group exists (count>0), which can leave required defaults missing in partially-seeded DBs. Ensure-by-name upserts are safer.

Proposed approach (in model/user_group.go, reference only):

var defaults = []UserGroup{
  {Name: "default", Description: "默认分组", Ratio: 1.0},
  {Name: "vip", Description: "VIP分组", Ratio: 1.0},
  {Name: "svip", Description: "SVIP分组", Ratio: 1.0},
}

func InitDefaultUserGroups() error {
  for _, g := range defaults {
    var count int64
    if err := DB.Model(&UserGroup{}).
      Where("name = ?", g.Name).
      Count(&count).Error; err != nil {
      return err
    }
    if count == 0 {
      if err := DB.Create(&g).Error; err != nil {
        return err
      }
    }
  }
  return nil
}

I can open a small follow-up PR if you’d like.

web/src/components/table/users/modals/AddUserModal.jsx (2)

56-71: Improve: surface backend error when fetching groups

Currently ignores non-success responses. Show message and keep UX consistent with catch path.

   const fetchGroups = async () => {
     try {
       const res = await API.get('/api/group/');
-      if (res.data.success) {
+      if (res.data.success) {
         setGroupOptions(
           res.data.data.map((group) => ({
             label: group,
             value: group,
           }))
         );
-      }
+      } else {
+        showError(res.data.message || t('获取分组列表失败'));
+      }
     } catch (error) {
-      showError(t('获取分组列表失败'));
+      showError(error);
     }
   };

194-204: Avoid free-form group creation from this modal

allowAdditions lets users submit non-existent groups; better to manage creation via “分组管理” to keep data consistent, or wire onCreate to the group-creation API.

 <Form.Select
   field='group'
   label={t('分组')}
   placeholder={t('请选择用户分组')}
   optionList={groupOptions}
   rules={[{ required: true, message: t('请选择分组') }]}
-  allowAdditions
   search
 />
web/src/helpers/api.js (2)

118-125: Remove duplicate 'group' key in payload

The second assignment is redundant (and can confuse readers), the first already sets it.

   const payload = {
     model: inputs.model,
     group: inputs.group,
     messages: processedMessages,
-    group: inputs.group,
     stream: inputs.stream,
   };

189-204: Deduplicate group-description translation logic

Same logic exists in render.jsx and UserGroupManagement.jsx. Extract a single helper to avoid drift.

Proposed shared helper (new file web/src/helpers/groups.js):

// web/src/helpers/groups.js
import i18next from 'i18next';

export function getSystemGroupDescription(groupName, originalDescription) {
  if (groupName === 'default' && originalDescription === '默认分组') return i18next.t('默认分组');
  if (groupName === 'vip' && originalDescription === 'VIP分组') return i18next.t('VIP分组');
  if (groupName === 'svip' && originalDescription === 'SVIP分组') return i18next.t('SVIP分组');
  return originalDescription;
}

And replace here:

-import i18next from 'i18next';
+import { getSystemGroupDescription } from './groups';
...
-const getGroupDescription = (groupName, originalDescription) => {
-  // 对于系统默认分组,使用翻译
-  if (groupName === 'default' && originalDescription === '默认分组') {
-    return i18next.t('默认分组');
-  }
-  if (groupName === 'vip' && originalDescription === 'VIP分组') {
-    return i18next.t('VIP分组');
-  }
-  if (groupName === 'svip' && originalDescription === 'SVIP分组') {
-    return i18next.t('SVIP分组');
-  }
-  // 对于用户自定义分组,使用原始描述
-  return originalDescription;
-};
+const getGroupDescription = getSystemGroupDescription;

Would you like me to propagate this change to render.jsx and UserGroupManagement.jsx?

web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (4)

109-144: DRY the default modules object

defaultModules is duplicated; extract a top-level DEFAULT_SIDEBAR_MODULES constant and reuse in reset and fallback.

Example:

const DEFAULT_SIDEBAR_MODULES = { /* current default structure */ };
// then use: setSidebarModulesAdmin(DEFAULT_SIDEBAR_MODULES);

Also applies to: 189-204


343-365: Simplify the user-module toggle handler

Inline object rebuilding is verbose; extract a small helper or use functional setState to avoid stale closure and improve readability.

- onChange={
-   module.key === 'user'
-     ? (checked) => {
-         const newModules = {
-           ...sidebarModulesAdmin,
-           [section.key]: {
-             ...sidebarModulesAdmin[section.key],
-             user: {
-               ...sidebarModulesAdmin[section.key].user,
-               enabled: checked
-             }
-           }
-         };
-         setSidebarModulesAdmin(newModules);
-       }
-     : handleModuleChange(section.key, module.key)
- }
+ onChange={
+   module.key === 'user'
+     ? (checked) =>
+         setSidebarModulesAdmin((prev) => ({
+           ...prev,
+           [section.key]: {
+             ...prev[section.key],
+             user: { ...prev[section.key].user, enabled: checked },
+           },
+         }))
+     : handleModuleChange(section.key, module.key)
+ }

371-377: Simplify condition for rendering the sub-switch

Redundant ternary can be reduced for clarity.

-{module.key === 'user' && (
-  module.key === 'user'
-    ? sidebarModulesAdmin[section.key]?.user?.enabled
-    : sidebarModulesAdmin[section.key]?.[module.key]
-) && (
+{module.key === 'user' && sidebarModulesAdmin[section.key]?.user?.enabled && (

410-419: Simplify disabled expression

No need for the inner ternary; directly check the nested user enabled flag.

- disabled={!sidebarModulesAdmin[section.key]?.enabled || !(
-   module.key === 'user'
-     ? sidebarModulesAdmin[section.key]?.user?.enabled
-     : sidebarModulesAdmin[section.key]?.[module.key]
- )}
+ disabled={
+   !sidebarModulesAdmin[section.key]?.enabled ||
+   !sidebarModulesAdmin[section.key]?.user?.enabled
+ }
web/src/components/table/users/UsersActions.jsx (1)

29-37: Normalize role to number before comparison

Ensure numeric comparisons aren’t tripped by string roles from storage.

-  return user?.role || 0;
+  return Number(user?.role) || 0;
web/src/components/table/users/modals/UserGroupManagement.jsx (2)

70-83: Optional: notify parent after deletion

After successful delete, also trigger onGroupUpdated to keep parent user list in sync.

   if (res.data.success) {
     showSuccess(t('删除成功'));
     loadGroups();
+    onGroupUpdated?.();
   } else {

117-131: Deduplicate group-description translation

Same translation function appears in multiple modules; import a shared helper to reduce maintenance.

controller/user_group.go (4)

3-14: Add strings import for upcoming validations.
We'll need strings.TrimSpace and strings.ToLower.

 import (
 	"encoding/json"
 	"fmt"
 	"strconv"
 
 	"one-api/common"
 	"one-api/model"
 	"one-api/setting"
 	"one-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
+	"strings"
 )

178-182: Reserved-name deletion check should be case-insensitive.
Also reuse the helper for consistency.

-	// 不允许删除默认分组
-	if group.Name == "default" || group.Name == "vip" || group.Name == "svip" {
+	// 不允许删除默认分组
+	if isReservedGroup(strings.ToLower(group.Name)) {
 		common.ApiErrorMsg(c, "不能删除系统默认分组")
 		return
 	}

208-229: Lost-update risk on settings writes.
Pattern: read-copy-modify-write the whole JSON can overwrite concurrent admin changes. Prefer an atomic “mutate under lock” API in ratio_setting that applies a single-key add/remove and persists once, or use optimistic concurrency (versioning) on options.


232-256: Same lost-update pattern for UserUsableGroups.
Apply the same atomic mutation approach as above to avoid clobbering concurrent edits.

model/user_group.go (2)

64-73: Consider case-insensitive duplicate checks.
Depending on DB collation, “VIP” vs “vip” may bypass uniqueness. Either normalize names to lower-case on write or enforce a functional index/collation. At minimum, lower both sides in the query if supported by your DB.


89-124: Default group seeding should ensure each reserved group exists.
Current logic returns when any group exists, potentially skipping creation of missing reserved groups.

-	// 检查是否已经存在默认分组
-	var count int64
-	DB.Model(&UserGroup{}).Count(&count)
-	if count > 0 {
-		return nil // 已经有分组了,不需要初始化
-	}
-
-	// 创建默认分组
+	// 创建缺失的默认分组
 	defaultGroups := []*UserGroup{
 		{
 			Name:        "default",
 			Description: "默认分组",
 			Ratio:       1.0,
 		},
 		{
 			Name:        "vip",
 			Description: "VIP分组",
 			Ratio:       1.0,
 		},
 		{
 			Name:        "svip",
 			Description: "SVIP分组",
 			Ratio:       1.0,
 		},
 	}
 
 	for _, group := range defaultGroups {
-		if err := group.Insert(); err != nil {
-			return err
-		}
+		var exist int64
+		if err := DB.Model(&UserGroup{}).Where("name = ?", group.Name).Count(&exist).Error; err != nil {
+			return err
+		}
+		if exist == 0 {
+			if err := group.Insert(); err != nil {
+				return err
+			}
+		}
 	}
 
 	return nil
📜 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 8cc5763 and 5a0b49e.

📒 Files selected for processing (25)
  • controller/user.go (9 hunks)
  • controller/user_group.go (1 hunks)
  • model/main.go (3 hunks)
  • model/user.go (5 hunks)
  • model/user_group.go (1 hunks)
  • router/api-router.go (1 hunks)
  • web/src/components/auth/AuthPageLayout.jsx (1 hunks)
  • web/src/components/auth/LoginForm.jsx (3 hunks)
  • web/src/components/auth/PasswordResetConfirm.jsx (3 hunks)
  • web/src/components/auth/PasswordResetForm.jsx (3 hunks)
  • web/src/components/auth/RegisterForm.jsx (3 hunks)
  • web/src/components/layout/Footer.jsx (1 hunks)
  • web/src/components/layout/HeaderBar/LanguageSelector.jsx (1 hunks)
  • web/src/components/settings/personal/components/UserInfoHeader.jsx (2 hunks)
  • web/src/components/table/users/UsersActions.jsx (1 hunks)
  • web/src/components/table/users/index.jsx (1 hunks)
  • web/src/components/table/users/modals/AddUserModal.jsx (4 hunks)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
  • web/src/helpers/api.js (2 hunks)
  • web/src/helpers/render.jsx (2 hunks)
  • web/src/i18n/locales/en.json (2 hunks)
  • web/src/pages/Home/index.jsx (1 hunks)
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (5 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
  • web/src/components/layout/HeaderBar/LanguageSelector.jsx
  • web/src/components/auth/RegisterForm.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • controller/user.go
🧰 Additional context used
🧬 Code graph analysis (16)
web/src/components/auth/PasswordResetForm.jsx (1)
web/src/components/auth/AuthPageLayout.jsx (1)
  • AuthPageLayout (22-37)
router/api-router.go (3)
middleware/auth.go (1)
  • AdminAuth (162-166)
controller/user_group.go (4)
  • GetAllUserGroups (17-24)
  • CreateUserGroup (27-75)
  • UpdateUserGroup (78-153)
  • DeleteUserGroup (156-205)
model/user_group.go (1)
  • GetAllUserGroups (38-42)
web/src/components/table/users/modals/AddUserModal.jsx (3)
web/src/helpers/api.js (7)
  • groupOptions (207-216)
  • res (245-245)
  • res (246-246)
  • res (289-289)
  • res (290-290)
  • API (30-38)
  • API (30-38)
web/src/components/table/users/modals/EditUserModal.jsx (3)
  • groupOptions (64-64)
  • getInitValues (69-81)
  • fetchGroups (83-90)
web/src/helpers/utils.jsx (1)
  • showError (118-147)
web/src/components/auth/LoginForm.jsx (1)
web/src/components/auth/AuthPageLayout.jsx (1)
  • AuthPageLayout (22-37)
web/src/components/table/users/modals/UserGroupManagement.jsx (5)
web/src/components/table/users/modals/EditUserGroupModal.jsx (4)
  • useTranslation (39-39)
  • loading (41-41)
  • getGroupDisplayName (51-56)
  • EditUserGroupModal (38-219)
web/src/helpers/render.jsx (1)
  • getGroupDisplayName (633-638)
web/src/helpers/api.js (7)
  • res (245-245)
  • res (246-246)
  • res (289-289)
  • res (290-290)
  • API (30-38)
  • API (30-38)
  • getGroupDescription (190-203)
web/src/helpers/utils.jsx (2)
  • showError (118-147)
  • showSuccess (153-155)
web/src/components/common/ui/CardTable.jsx (1)
  • CardTable (42-232)
web/src/components/auth/PasswordResetConfirm.jsx (1)
web/src/components/auth/AuthPageLayout.jsx (1)
  • AuthPageLayout (22-37)
model/user_group.go (4)
common/utils.go (1)
  • GetTimestamp (192-194)
model/main.go (1)
  • DB (63-63)
controller/user_group.go (1)
  • GetAllUserGroups (17-24)
model/user.go (1)
  • User (20-49)
web/src/helpers/api.js (2)
web/src/components/table/users/modals/UserGroupManagement.jsx (1)
  • getGroupDescription (118-131)
web/src/components/table/users/modals/AddUserModal.jsx (1)
  • groupOptions (45-45)
web/src/components/table/users/index.jsx (1)
web/src/components/table/users/UsersActions.jsx (1)
  • UsersActions (25-109)
web/src/helpers/render.jsx (2)
web/src/components/table/users/modals/EditUserGroupModal.jsx (1)
  • getGroupDisplayName (51-56)
web/src/components/table/users/modals/UserGroupManagement.jsx (1)
  • getGroupDisplayName (110-115)
controller/user_group.go (6)
model/user_group.go (5)
  • GetAllUserGroups (38-42)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (65-73)
  • GetUserGroupById (45-52)
  • IsUserGroupInUse (76-80)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/user.go (1)
  • UpdateUsersGroupName (925-977)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
web/src/components/table/users/UsersActions.jsx (1)
web/src/components/table/users/modals/UserGroupManagement.jsx (1)
  • UserGroupManagement (46-296)
web/src/components/table/users/modals/EditUserGroupModal.jsx (4)
web/src/components/table/users/modals/AddUserModal.jsx (7)
  • useTranslation (42-42)
  • formApiRef (43-43)
  • loading (44-44)
  • getInitValues (48-54)
  • submit (73-86)
  • handleCancel (88-90)
  • Typography (39-39)
web/src/components/table/users/modals/UserGroupManagement.jsx (4)
  • useTranslation (47-47)
  • loading (49-49)
  • editingGroup (52-52)
  • getGroupDisplayName (110-115)
web/src/helpers/api.js (7)
  • url (258-258)
  • res (245-245)
  • res (246-246)
  • res (289-289)
  • res (290-290)
  • API (30-38)
  • API (30-38)
web/src/helpers/utils.jsx (2)
  • showSuccess (153-155)
  • showError (118-147)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • handleModuleChange (109-120)
model/main.go (2)
model/user_group.go (2)
  • UserGroup (8-16)
  • InitDefaultUserGroups (90-124)
common/sys_log.go (1)
  • SysLog (10-13)
model/user.go (5)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/constants.go (2)
  • QuotaForInvitee (100-100)
  • QuotaForInviter (99-99)
model/log.go (1)
  • RecordLog (78-94)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
🔇 Additional comments (13)
web/src/components/auth/LoginForm.jsx (2)

48-49: LGTM: imports

AuthPageLayout import is correct and localized to auth components.


607-634: Verify scroll behavior with new wrapper

AuthPageLayout uses overflow-hidden; ensure long content/modals/Turnstile don’t get clipped on small screens. If needed, switch to min-h-screen + overflow-y-auto in the layout as suggested.

I can provide a quick CSS tweak PR if you confirm any clipping on iPhone SE/Android small viewports.

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

161-161: LGTM – border removal is consistent with Footer’s new top border.

Visually reduces double separators with the new footer border. No functional impact.

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

2082-2126: Group management strings look consistent and complete.

Covers create/edit/validation flows and system group hints; matches modal usage.

web/src/components/table/users/index.jsx (1)

91-95: LGTM: wiring refresh into UsersActions.

Prop plumb-through is correct and matches UsersActions signature; enables child-driven refresh after group updates.

router/api-router.go (1)

203-210: LGTM: admin-protected user_group CRUD routes.

Routes are grouped under /api/user_group with AdminAuth; complements controller methods and stays within existing rate limit middleware.

model/main.go (1)

293-296: LGTM: include UserGroup in migrations (normal and fast paths).

Ensures schema creation across paths.

Also applies to: 340-341

web/src/components/table/users/modals/AddUserModal.jsx (2)

92-97: LGTM: lazy-load groups on open

Fetching options when the modal becomes visible avoids stale/unused requests. Looks good.


52-52: Verify default group and endpoint usage

  • Ensure the backend’s GET /api/group/ returns a “default” group to match the hard-coded default.
  • Confirm that /api/group/ (for fetching groups) and /api/user_group (for creating/updating user-group relations) are both valid and intentional.
web/src/helpers/api.js (1)

206-216: LGTM: translated labels for groups

Using translatedDesc for both truncated label and fullLabel improves UX without altering data values.

web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (2)

58-61: LGTM: nested admin.user configuration

The nested structure (enabled + groupManagement) is a good foundation for fine-grained gating.


94-107: LGTM: isolated handler for groupManagement

Clear, localized state update for the nested flag.

web/src/components/table/users/UsersActions.jsx (1)

100-107: LGTM: modal gated with same predicate as button

Keeps render consistent with the permission check.

Comment thread controller/user_group.go
Comment thread controller/user_group.go Outdated
Comment thread controller/user_group.go Outdated
Comment thread controller/user_group.go Outdated
Comment thread controller/user_group.go Outdated
Comment thread web/src/components/table/users/modals/EditUserGroupModal.jsx Outdated
Comment thread web/src/components/table/users/modals/EditUserGroupModal.jsx
Comment thread web/src/components/table/users/modals/EditUserGroupModal.jsx
Comment thread web/src/components/table/users/modals/UserGroupManagement.jsx Outdated
Comment thread web/src/components/table/users/UsersActions.jsx

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

🧹 Nitpick comments (3)
web/src/hooks/common/useSidebar.js (3)

24-27: Make the event bus SSR/HMR-safe and reusable across module reloads

Great call introducing a lightweight event bus. Guard it for SSR and reuse a singleton during HMR to avoid split buses.

Apply this diff:

-// 创建一个全局事件系统来同步所有useSidebar实例
-const sidebarEventTarget = new EventTarget();
+// 创建一个全局事件系统来同步所有useSidebar实例(SSR/HMR 安全,单例)
+const sidebarEventTarget =
+  (typeof window !== 'undefined'
+    ? (window.__SIDEBAR_EVENT_TARGET__ ||= new EventTarget())
+    : new EventTarget());
 const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';

If cross-tab sync is desired (multiple browser tabs), consider also broadcasting via BroadcastChannel('sidebar') and listening alongside EventTarget.


128-133: Prevent double-fetch on the origin instance; include event.detail and skip self

Dispatching a global event causes the emitting hook to refresh twice (local await loadUserConfig + listener-triggered loadUserConfig). Tag the event with a per-instance id and ignore self in the handler.

Apply this diff:

-    // 移除adminConfig的条件限制,直接刷新用户配置
-    await loadUserConfig();
-
-    // 触发全局刷新事件,通知所有useSidebar实例更新
-    sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
+    // 移除adminConfig的条件限制,直接刷新用户配置
+    await loadUserConfig();
+    // 触发全局刷新事件,通知所有useSidebar实例更新;附带来源以避免自身重复请求
+    sidebarEventTarget.dispatchEvent(
+      new CustomEvent(SIDEBAR_REFRESH_EVENT, { detail: { source: instanceId.current } })
+    );

Add this outside the selected range:

// imports: add useRef
// import { useState, useEffect, useMemo, useContext } from 'react';
import { useState, useEffect, useMemo, useContext, useRef } from 'react';

// inside useSidebar(), near the top:
const instanceId = useRef(Symbol('useSidebar'));

143-157: Skip self-originated events and tighten the handler

Without filtering, the emitter also re-fetches. Ignore events from the same instance; optionally wrap loadUserConfig in useCallback([adminConfig]) to avoid stale closures.

Apply this diff:

-  useEffect(() => {
-    const handleRefresh = () => {
-      if (Object.keys(adminConfig).length > 0) {
-        loadUserConfig();
-      }
-    };
-
-    sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
-
-    return () => {
-      sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
-    };
-  }, [adminConfig]);
+  useEffect(() => {
+    const handleRefresh = (e) => {
+      // 忽略自身触发的刷新事件,避免重复请求
+      if (e?.detail?.source === instanceId.current) return;
+      if (Object.keys(adminConfig).length > 0) {
+        loadUserConfig();
+      }
+    };
+    sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
+    return () => {
+      sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
+    };
+  }, [adminConfig]);
📜 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 5a0b49e and ef9c386.

📒 Files selected for processing (2)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (5 hunks)
  • web/src/hooks/common/useSidebar.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx

@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

Caution

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

⚠️ Outside diff range comments (4)
web/src/components/table/users/modals/AddUserModal.jsx (1)

73-86: Critical: submit lacks try/catch; loading can stick and errors are swallowed.
Wrap API call in try/catch/finally.

   const submit = async (values) => {
-    setLoading(true);
-    const res = await API.post(`/api/user/`, values);
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess(t('用户账户创建成功!'));
-      formApiRef.current?.setValues(getInitValues());
-      props.refresh();
-      props.handleClose();
-    } else {
-      showError(message);
-    }
-    setLoading(false);
+    setLoading(true);
+    try {
+      const res = await API.post('/api/user/', values);
+      const { success, message } = res.data;
+      if (success) {
+        showSuccess(t('用户账户创建成功!'));
+        formApiRef.current?.setValues(getInitValues());
+        props.refresh();
+        props.handleClose();
+      } else {
+        showError(message || t('用户创建失败'));
+      }
+    } catch (error) {
+      showError(error);
+    } finally {
+      setLoading(false);
+    }
   };
model/user.go (3)

400-411: Avoid extra read and double write when initializing sidebar config.

Use the freshly inserted user (GORM fills user.Id) and update only the setting column to reduce round-trips and contention.

-  // 用户创建成功后,根据角色初始化边栏配置
-  // 需要重新获取用户以确保有正确的ID和Role
-  var createdUser User
-  if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil {
-    // 生成基于角色的默认边栏配置
-    defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(createdUser.Role)
-    if defaultSidebarConfig != "" {
-      currentSetting := createdUser.GetSetting()
-      currentSetting.SidebarModules = defaultSidebarConfig
-      createdUser.SetSetting(currentSetting)
-      createdUser.Update(false)
-      common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
-    }
-  }
+  // 用户创建成功后,根据角色初始化边栏配置(单次更新)
+  defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(user.Role)
+  if defaultSidebarConfig != "" {
+    currentSetting := user.GetSetting()
+    currentSetting.SidebarModules = defaultSidebarConfig
+    user.SetSetting(currentSetting)
+    if err := DB.Model(user).Update("setting", user.Setting).Error; err != nil {
+      common.SysLog("初始化边栏配置失败: " + err.Error())
+    } else {
+      common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", user.Username, user.Role))
+    }
+  }

823-826: **GORM misuse: passing User to First causes incorrect scanning.

First(&user) where user is *User passes **User. Initialize a value and return its address.

-func GetRootUser() (user *User) {
-  DB.Where("role = ?", common.RoleRootUser).First(&user)
-  return user
-}
+func GetRootUser() *User {
+  var user User
+  if err := DB.Where("role = ?", common.RoleRootUser).First(&user).Error; err != nil {
+    return nil
+  }
+  return &user
+}

727-756: Avoid shadowing setting and simplify the cache branch

Replace the short-declaration in the Redis check with a new local name and return the cached dto.UserSetting directly, for example:

if !fromDB && common.RedisEnabled {
    cached, err := getUserSettingCache(id)
    if err == nil {
        return cached, nil
    }
    // fall through to DB
}

No wrapping in UserBase.GetSetting is required since getUserSettingCache already returns dto.UserSetting.

♻️ Duplicate comments (5)
web/src/components/table/users/modals/EditUserGroupModal.jsx (3)

1-17: License header inconsistent with repo (AGPL vs Apache); align to project standard.
Replace Apache-2.0 header and incorrect owner spelling with the repo’s AGPL v3 header and “QuantumNous”.

-/*
-Copyright 2024 Quantumnous Inc.
-...
-Licensed under the Apache License, Version 2.0 (the "License");
-...
-For commercial licensing, please contact support@quantumnous.com
-*/
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/

58-63: Bug: 0 ratio becomes 1.0 due to || fallback.
Use nullish coalescing to preserve 0.

   const getInitValues = () => ({
     name: editingGroup?.name || '',
     description: editingGroup?.description || '',
-    ratio: editingGroup?.ratio || 1.0,
+    ratio: editingGroup?.ratio ?? 1.0,
   });

64-71: Bug: parseFloat(... ) || 1.0 turns 0 into 1.0; handle NaN explicitly.
Preserve 0; default only when NaN/undefined.

-      const data = {
-        ...values,
-        ratio: parseFloat(values.ratio) || 1.0,
-      };
+      const ratioNum =
+        typeof values.ratio === 'number' ? values.ratio : parseFloat(values.ratio);
+      const data = {
+        ...values,
+        ratio: Number.isFinite(ratioNum) ? ratioNum : 1.0,
+      };
web/src/components/table/users/UsersActions.jsx (1)

58-66: Fail closed on config parse errors or absence.
Don’t grant access when config is missing/invalid.

         } catch (error) {
           console.error('解析侧边栏配置失败:', error);
-          return true; // 解析失败时默认允许访问
+          return false; // 解析失败时默认禁止访问
         }
       }
-      return true; // 没有配置时默认允许访问
+      return false; // 没有配置时默认禁止访问
model/user.go (1)

924-977: Rename groups: add cache invalidation, no-op detection, and input sanity.

  • No cache refresh → stale reads via Redis. Invalidate affected users post-commit.
  • Return a clear error if oldGroupName == newGroupName or when no rows are updated.
 func UpdateUsersGroupName(oldGroupName, newGroupName string) error {
   if oldGroupName == "" || newGroupName == "" {
     return errors.New("分组名称不能为空")
   }
+  if oldGroupName == newGroupName {
+    return errors.New("新旧分组名相同,无需更新")
+  }

   common.SysLog(fmt.Sprintf("开始更新用户分组名称: '%s' -> '%s'", oldGroupName, newGroupName))

@@
   result := tx.Model(&User{}).Where(commonGroupCol+" = ?", oldGroupName).Update(commonGroupCol, newGroupName)
   if result.Error != nil {
     tx.Rollback()
     common.SysLog(fmt.Sprintf("更新用户分组名称失败: %s", result.Error.Error()))
     return result.Error
   }
+  if result.RowsAffected == 0 {
+    tx.Rollback()
+    return fmt.Errorf("未找到使用分组 '%s' 的用户", oldGroupName)
+  }

   // 提交事务
   if err := tx.Commit().Error; err != nil {
     common.SysLog(fmt.Sprintf("提交事务失败: %s", err.Error()))
     return err
   }

   common.SysLog(fmt.Sprintf("成功更新 %d 个用户的分组名称从 '%s' 到 '%s'", result.RowsAffected, oldGroupName, newGroupName))

+  // 刷新缓存(异步),避免读取旧分组
+  if common.RedisEnabled {
+    gopool.Go(func() {
+      var ids []int
+      if err := DB.Model(&User{}).Where(commonGroupCol+" = ?", newGroupName).Pluck("id", &ids).Error; err == nil {
+        for _, id := range ids {
+          _ = invalidateUserCache(id)
+        }
+      }
+    })
+  }

If commonGroupCol can vary, ensure it’s a trusted constant to avoid SQL injection via column name concatenation.

🧹 Nitpick comments (14)
web/src/components/table/users/modals/EditUserGroupModal.jsx (2)

76-79: Nit: remove redundant url ternary.
Same string both sides.

-      const url = isEdit ? '/api/user_group' : '/api/user_group';
-      const method = isEdit ? 'PUT' : 'POST';
-      
-      const res = await API[method.toLowerCase()](url, data);
+      const method = isEdit ? 'PUT' : 'POST';
+      const res = await API[method.toLowerCase()]('/api/user_group', data);

88-90: Prefer surfacing actual error details.
Pass the caught error to showError for actionable diagnostics.

-    } catch (error) {
-      showError(isEdit ? t('分组更新失败') : t('分组创建失败'));
-    }
+    } catch (error) {
+      showError(error);
+    }
web/src/components/table/users/modals/AddUserModal.jsx (1)

194-204: Avoid allowing arbitrary new group names unless backend supports it.
Remove allowAdditions or handle onCreate; otherwise users can submit non-existent groups.

                 <Form.Select
                   field='group'
                   label={t('分组')}
                   placeholder={t('请选择用户分组')}
                   optionList={groupOptions}
                   rules={[{ required: true, message: t('请选择分组') }]}
-                  allowAdditions
                   search
                 />
web/src/components/table/users/UsersActions.jsx (2)

29-37: Optional: derive role from trusted context when available.
Prefer statusState.user?.role over localStorage to reduce spoofable UI gating (backend must still enforce auth).

-  const getUserRole = () => {
-    const user = JSON.parse(localStorage.getItem('user') || '{}');
-    return user?.role || 0;
-  };
+  const getUserRole = () => statusState?.user?.role ?? (() => {
+    const user = JSON.parse(localStorage.getItem('user') || '{}');
+    return user?.role || 0;
+  })();

82-101: Nit: avoid double compute of canShowGroupManagement().
Store result in a const to skip repeated JSON.parse per render.

-    <>
-      <div className='flex gap-2 w-full md:w-auto order-2 md:order-1'>
+    <>
+      {/** compute once per render */}
+      {(() => { const canGroup = canShowGroupManagement(); return (
+      <div className='flex gap-2 w-full md:w-auto order-2 md:order-1'>
         <Button className='w-full md:w-auto' onClick={handleAddUser} size='small'>
           {t('添加用户')}
         </Button>
-        {canShowGroupManagement() && (
+        {canGroup && (
           <Button
             className='w-full md:w-auto'
             onClick={handleGroupManagement}
             size='small'
             theme='light'
           >
             {t('分组管理')}
           </Button>
         )}
       </div>
-
-      {canShowGroupManagement() && (
-        <UserGroupManagement
-          visible={showGroupManagement}
-          onClose={() => setShowGroupManagement(false)}
-          onGroupUpdated={onRefreshUsers}
-        />
-      )}
+      )}
+      )() }
+      {(() => { const canGroup = canShowGroupManagement(); return canGroup ? (
+        <UserGroupManagement visible={showGroupManagement}
+          onClose={() => setShowGroupManagement(false)} onGroupUpdated={onRefreshUsers} />
+      ) : null; })()}
web/src/components/settings/personal/components/UserInfoHeader.jsx (1)

162-162: Deduplicate group label logic; use the shared helper for consistency

Use the central helper (maps 'default' → localized) instead of inlining the ternary twice to keep behavior consistent across the app and handle future mapping tweaks in one place.

Apply:

+ import { getGroupDisplayName } from '../../../../helpers/render';
- {userState?.user?.group === 'default' ? t('默认') : (userState?.user?.group || t('默认'))}
+ {getGroupDisplayName(userState?.user?.group)}

Also applies to: 210-210

web/src/hooks/common/useSidebar.js (1)

24-27: Harden EventTarget creation for SSR/non-browser environments

If this hook is imported during SSR, new EventTarget() may not exist. Provide a minimal no-op fallback.

Apply:

-// 创建一个全局事件系统来同步所有useSidebar实例
-const sidebarEventTarget = new EventTarget();
+// 创建一个全局事件系统来同步所有useSidebar实例(SSR 兼容)
+const sidebarEventTarget =
+  typeof window !== 'undefined' && typeof window.EventTarget !== 'undefined'
+    ? new EventTarget()
+    : { addEventListener() {}, removeEventListener() {}, dispatchEvent() {} };
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (3)

102-116: Scoped handler for groupManagement is fine

Updates only admin.user.groupManagement without affecting siblings. Consider consolidating nested updates via a small helper to reduce repetition if more sub-switches arrive.


393-413: Extract inline onChange handler to avoid re-creating functions per render

Define a named handler for toggling user.enabled to reduce churn and improve readability.

Example:

+ const handleUserEnabledChange = (sectionKey) => (checked) => {
+   setSidebarModulesAdmin(prev => ({
+     ...prev,
+     [sectionKey]: {
+       ...prev[sectionKey],
+       user: { ...prev[sectionKey].user, enabled: checked },
+     },
+   }));
+ };
...
- onChange={
-   module.key === 'user'
-     ? (checked) => { /* ... */ }
-     : handleModuleChange(section.key, module.key)
- }
+ onChange={
+   module.key === 'user'
+     ? handleUserEnabledChange(section.key)
+     : handleModuleChange(section.key, module.key)
+ }

420-472: Simplify conditionals and reuse computed booleans

The nested ternary repeats the same check. Compute once for clarity; behavior unchanged.

Apply:

- {module.key === 'user' && (
-   module.key === 'user'
-     ? sidebarModulesAdmin[section.key]?.user?.enabled
-     : sidebarModulesAdmin[section.key]?.[module.key]
- ) && (
+ {module.key === 'user' && sidebarModulesAdmin[section.key]?.user?.enabled && (
    <div style={{ borderTop: '1px solid var(--semi-color-border)', marginTop: '12px', paddingTop: '12px' }}>
      ...
-     <Switch
-       checked={sidebarModulesAdmin[section.key]?.user?.groupManagement || false}
-       onChange={handleUserGroupManagementChange}
-       size="small"
-       disabled={!sidebarModulesAdmin[section.key]?.enabled || !(
-         module.key === 'user'
-           ? sidebarModulesAdmin[section.key]?.user?.enabled
-           : sidebarModulesAdmin[section.key]?.[module.key]
-       )}
-     />
+     <Switch
+       checked={Boolean(sidebarModulesAdmin[section.key]?.user?.groupManagement)}
+       onChange={handleUserGroupManagementChange}
+       size="small"
+       disabled={
+         !sidebarModulesAdmin[section.key]?.enabled ||
+         !sidebarModulesAdmin[section.key]?.user?.enabled
+       }
+     />
    </div>
  )}
web/src/components/auth/ModuleRoute.jsx (4)

39-76: Prevent setState on unmounted and reduce repeated parsing.

Guard async state updates and centralize localStorage parsing.

-  useEffect(() => {
-    checkModulePermission();
-  }, [modulePath, statusState?.status]); // 只在status数据变化时重新检查
+  const getUserFromStorage = () => {
+    try { return JSON.parse(localStorage.getItem('user') || 'null'); } catch { return null; }
+  };
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      const ok = await checkModulePermission(getUserFromStorage());
+      if (!cancelled) setHasPermission(ok);
+    })();
+    return () => { cancelled = true; };
+  }, [modulePath, statusState?.status]);
 
-  const checkModulePermission = async () => {
+  const checkModulePermission = async (userObj) => {
     try {
-      // 检查用户是否已登录
-      const user = localStorage.getItem('user');
-      if (!user) {
+      if (!userObj) {
         setHasPermission(false);
-        return;
+        return false;
       }
-
-      const userData = JSON.parse(user);
-      const userRole = userData.role;
+      const userRole = userObj.role;
@@
-      setHasPermission(permission);
+      return permission;
     } catch (error) {
       console.error('检查模块权限失败:', error);
-      // 出错时采用安全优先策略,拒绝访问
-      setHasPermission(false);
+      return false;
     }
   };

110-128: Avoid N+1 calls to /api/user/self across many ModuleRoute instances.

Reuse config from a context/hook (e.g., useSidebar’s merged “final” config) or share a memoized fetch to prevent multiple identical requests per route.


55-60: Role thresholds: align with backend constants to avoid drift.

UI uses >= 100 for super-admin and >= 10 && < 100 for admin, while backend checks equality against common.RoleAdminUser/common.RoleRootUser. Recommend defining FE role constants and using equality to match BE.

Proposed in FE constants:

export const Role = { USER: 1, ADMIN: 10, ROOT: 100 };

Then compare with ===.

Also applies to: 100-112


91-99: Defensive checks are good; consider fallbacks for unknown module paths.

Current parts.length !== 2 returns false. Optionally log once to help diagnose misconfigured modulePath.

Also applies to: 160-184

📜 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 ac60739 and d798db5.

📒 Files selected for processing (35)
  • controller/misc.go (1 hunks)
  • controller/user.go (9 hunks)
  • controller/user_group.go (1 hunks)
  • model/main.go (3 hunks)
  • model/option.go (1 hunks)
  • model/user.go (5 hunks)
  • model/user_group.go (1 hunks)
  • router/api-router.go (1 hunks)
  • setting/operation_setting/general_setting.go (1 hunks)
  • web/src/components/auth/AuthPageLayout.jsx (1 hunks)
  • web/src/components/auth/LoginForm.jsx (3 hunks)
  • web/src/components/auth/ModuleRoute.jsx (1 hunks)
  • web/src/components/auth/PasswordResetConfirm.jsx (3 hunks)
  • web/src/components/auth/PasswordResetForm.jsx (3 hunks)
  • web/src/components/auth/RegisterForm.jsx (3 hunks)
  • web/src/components/layout/Footer.jsx (1 hunks)
  • web/src/components/layout/headerbar/LanguageSelector.jsx (1 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (5 hunks)
  • web/src/components/settings/personal/components/UserInfoHeader.jsx (2 hunks)
  • web/src/components/table/users/UsersActions.jsx (1 hunks)
  • web/src/components/table/users/index.jsx (1 hunks)
  • web/src/components/table/users/modals/AddUserModal.jsx (4 hunks)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
  • web/src/components/topup/index.jsx (6 hunks)
  • web/src/helpers/api.js (2 hunks)
  • web/src/helpers/render.jsx (2 hunks)
  • web/src/helpers/utils.jsx (1 hunks)
  • web/src/hooks/common/useSidebar.js (3 hunks)
  • web/src/hooks/common/useUserPermissions.js (1 hunks)
  • web/src/i18n/locales/en.json (3 hunks)
  • web/src/pages/Home/index.jsx (1 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (3 hunks)
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (5 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • web/src/components/layout/headerbar/LanguageSelector.jsx
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
  • web/src/hooks/common/useUserPermissions.js
🚧 Files skipped from review as they are similar to previous changes (23)
  • web/src/pages/Home/index.jsx
  • model/option.go
  • model/main.go
  • web/src/components/auth/PasswordResetConfirm.jsx
  • web/src/helpers/render.jsx
  • controller/misc.go
  • controller/user_group.go
  • web/src/helpers/api.js
  • web/src/components/auth/AuthPageLayout.jsx
  • controller/user.go
  • setting/operation_setting/general_setting.go
  • web/src/components/layout/Footer.jsx
  • web/src/components/settings/personal/cards/NotificationSettings.jsx
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx
  • web/src/components/auth/RegisterForm.jsx
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/table/users/modals/UserGroupManagement.jsx
  • web/src/components/auth/PasswordResetForm.jsx
  • router/api-router.go
  • web/src/helpers/utils.jsx
  • web/src/components/topup/index.jsx
  • model/user_group.go
  • web/src/i18n/locales/en.json
🧰 Additional context used
🧬 Code graph analysis (8)
web/src/components/auth/ModuleRoute.jsx (2)
web/src/hooks/common/useSidebar.js (1)
  • finalConfig (163-208)
web/src/components/common/ui/Loading.jsx (1)
  • Loading (23-29)
web/src/components/table/users/index.jsx (1)
web/src/components/table/users/UsersActions.jsx (1)
  • UsersActions (25-109)
model/user.go (4)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
model/log.go (1)
  • RecordLog (78-94)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
web/src/components/table/users/modals/EditUserGroupModal.jsx (3)
web/src/components/table/users/modals/UserGroupManagement.jsx (1)
  • getGroupDisplayName (110-115)
web/src/helpers/api.js (6)
  • res (245-245)
  • res (246-246)
  • res (289-289)
  • res (290-290)
  • API (30-38)
  • API (30-38)
web/src/helpers/utils.jsx (2)
  • showSuccess (153-155)
  • showError (118-147)
web/src/components/settings/personal/components/UserInfoHeader.jsx (2)
web/src/components/auth/LoginForm.jsx (1)
  • userState (61-61)
web/src/components/auth/RegisterForm.jsx (1)
  • userState (68-68)
web/src/components/table/users/modals/AddUserModal.jsx (3)
web/src/helpers/api.js (7)
  • groupOptions (207-216)
  • res (245-245)
  • res (246-246)
  • res (289-289)
  • res (290-290)
  • API (30-38)
  • API (30-38)
web/src/components/table/users/modals/EditUserModal.jsx (3)
  • groupOptions (64-64)
  • getInitValues (69-81)
  • fetchGroups (83-90)
web/src/helpers/utils.jsx (1)
  • showError (118-147)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • handleModuleChange (118-129)
web/src/components/table/users/UsersActions.jsx (4)
web/src/components/layout/Footer.jsx (1)
  • statusState (31-31)
web/src/pages/Home/index.jsx (1)
  • statusState (70-70)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
  • statusState (39-39)
web/src/components/table/users/modals/UserGroupManagement.jsx (1)
  • UserGroupManagement (46-296)
🔇 Additional comments (7)
web/src/components/table/users/modals/EditUserGroupModal.jsx (1)

36-37: helpers barrel correctly re-exports API, showError, and showSuccess

web/src/components/table/users/index.jsx (1)

91-95: LGTM: Refresh flow correctly threaded.
Passing refresh as onRefreshUsers keeps the list in sync after group changes.

web/src/components/table/users/modals/AddUserModal.jsx (1)

56-71: No changes needed for group endpoint
GET /api/group/ is defined in router/api-router.go and consistently used by AddUserModal and EditUserModal—/api/group/ remains valid.

web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (3)

95-96: Module-level toggle update looks correct

Directly flips the module flag under the selected section; since 'user' is handled specially elsewhere, this won’t clobber the nested structure.


143-148: Defaults include groupManagement; good

Keeps backward-compatible defaults while exposing the new control.


213-218: Fallback default on load error includes nested user; good

Prevents shape drift when options parsing fails.

model/user.go (1)

96-96: All call sites updated and no dead references remain. Verified that the unexported generateDefaultSidebarConfigForRole is no longer referenced and all calls use GenerateDefaultSidebarConfigForRole.

Comment thread model/user.go
Comment thread web/src/components/table/users/modals/AddUserModal.jsx
Comment thread web/src/hooks/common/useSidebar.js Outdated
Comment thread web/src/hooks/common/useSidebar.js Outdated
Comment thread web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
@x-Ai x-Ai changed the title 新增"邀请"功能开关、"分组管理"按扭 新增"邀请"功能开关、"分组管理",修复"边栏"、"顶栏"、"分组"权限问题 Sep 2, 2025
@x-Ai x-Ai changed the title 新增"邀请"功能开关、"分组管理",修复"边栏"、"顶栏"、"分组"权限问题 feat: 新增邀请开关与分组管理,并修复权限同步及UI样式问题 Sep 2, 2025

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

Caution

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

⚠️ Outside diff range comments (1)
web/src/pages/Home/index.jsx (1)

84-96: Sanitize server-rendered HTML before dangerouslySetInnerHTML.

Marked output is injected directly; without sanitization this is XSS-prone if backend content is compromised.

Apply:

@@
-      if (!data.startsWith('https://')) {
-        content = marked.parse(data);
-      }
+      if (!data.startsWith('https://')) {
+        content = DOMPurify.sanitize(marked.parse(data));
+      }

And add the import:

 import { marked } from 'marked';
+import DOMPurify from 'dompurify';

Optional hardening for the iframe branch: use a sandbox (adjust allowlist as needed):

<iframe
  src={homePageContent}
  className='w-full flex-1 border-none'
  sandbox='allow-scripts allow-same-origin allow-popups'
/>

Also applies to: 353-356

♻️ Duplicate comments (10)
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)

235-245: Fix Chinese typo and update i18n key (“不在” → “不再”)

-                  extraText={t('关闭后:不在启用邀请奖励功能')}
+                  extraText={t('关闭后:不再启用邀请奖励功能')}

Also update locales:

  • en.json: rename key to 关闭后:不再启用邀请奖励功能 (keep same English value).
  • zh.json: add "关闭后:不再启用邀请奖励功能": "关闭后:不再启用邀请奖励功能".
#!/bin/bash
rg -n "关闭后:不在启用邀请奖励功能|关闭后:不再启用邀请奖励功能" web/src/i18n/locales/*.json
web/src/components/topup/index.jsx (1)

265-301: Unsafe fallback: enabling invitation on failure leaks UI and triggers failing calls

On /api/status failure/exception you set invitationEnabled=true and fetch aff link. Default closed instead; don’t call getAffLink unless enabled is true.

       if (success) {
@@
         if (enabled && !affFetchedRef.current) {
           affFetchedRef.current = true;
           getAffLink();
         }
       } else {
-        // API调用失败,使用后端默认值(true)
-        //console.log('status接口调用失败,使用默认值true');
-        setInvitationEnabled(true);
-        setInvitationConfigLoaded(true);
-        if (!affFetchedRef.current) {
-          affFetchedRef.current = true;
-          getAffLink();
-        }
+        // 安全降级为关闭
+        setInvitationEnabled(false);
+        setInvitationConfigLoaded(true);
       }
     } catch (error) {
-      // 出错时使用后端默认值(true)
-      setInvitationEnabled(true);
-      setInvitationConfigLoaded(true);
-      if (!affFetchedRef.current) {
-        affFetchedRef.current = true;
-        getAffLink();
-      }
+      // 出错时安全降级为关闭
+      setInvitationEnabled(false);
+      setInvitationConfigLoaded(true);
     }
model/user.go (2)

424-436: Always count successful invites even when reward quota is 0

Currently inviteUser runs only when QuotaForInviter > 0, so AffCount won’t increase if reward is disabled. Move inviteUser outside the quota block; keep reward log guarded.

Apply:

-      if common.QuotaForInviter > 0 {
-        //_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
-        RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
-        _ = inviteUser(inviterId)
-      }
+      if common.QuotaForInviter > 0 {
+        //_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
+        RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
+      }
+      // 始终累计邀请次数
+      _ = inviteUser(inviterId)

930-983: Invalidate caches and handle no-op/same-name cases after group rename

Rename doesn’t refresh Redis-backed caches; also treat same-name/no-op updates explicitly.

Apply:

 func UpdateUsersGroupName(oldGroupName, newGroupName string) error {
   if oldGroupName == "" || newGroupName == "" {
     return errors.New("分组名称不能为空")
   }
+  if oldGroupName == newGroupName {
+    return errors.New("新旧分组名称相同,无需更新")
+  }

   common.SysLog(fmt.Sprintf("开始更新用户分组名称: '%s' -> '%s'", oldGroupName, newGroupName))

   // 先查询有多少用户使用旧分组名
   var count int64
   if err := DB.Model(&User{}).Where(commonGroupCol+" = ?", oldGroupName).Count(&count).Error; err != nil {
     common.SysLog(fmt.Sprintf("查询使用分组 '%s' 的用户数量失败: %s", oldGroupName, err.Error()))
     return err
   }
   common.SysLog(fmt.Sprintf("找到 %d 个用户使用分组 '%s'", count, oldGroupName))
+  if count == 0 {
+    return gorm.ErrRecordNotFound
+  }

   // 使用事务确保数据一致性
   tx := DB.Begin()
@@
   if err := tx.Commit().Error; err != nil {
     common.SysLog(fmt.Sprintf("提交事务失败: %s", err.Error()))
     return err
   }

   common.SysLog(fmt.Sprintf("成功更新 %d 个用户的分组名称从 '%s' 到 '%s'", result.RowsAffected, oldGroupName, newGroupName))
+  if result.RowsAffected == 0 {
+    return gorm.ErrRecordNotFound
+  }

+  // 刷新相关用户缓存(异步)
+  if common.RedisEnabled {
+    gopool.Go(func() {
+      var ids []int
+      if err := DB.Model(&User{}).Where(commonGroupCol+" = ?", newGroupName).Pluck("id", &ids).Error; err == nil {
+        for _, id := range ids {
+          _ = invalidateUserCache(id)
+        }
+      }
+    })
+  }
web/src/components/table/users/UsersActions.jsx (1)

39-66: Fail-closed when server-side sidebar_config is absent

Today useSidebar falls back to a permissive default; admins may see “分组管理” even if server didn’t return permissions. Gate admin visibility on server-sourced config.

Apply (requires hook support):

-  const { finalConfig, loading: sidebarLoading } = useSidebar();
+  const { finalConfig, loading: sidebarLoading, hasServerConfig } = useSidebar();
@@
-    if (isAdmin()) {
+    if (isAdmin()) {
+      // 没有服务端权限配置时默认拒绝
+      if (!hasServerConfig) return false;
       const userSection = finalConfig?.admin?.user;
       if (!userSection || userSection.enabled === false) {
         return false;
       }
       return userSection.groupManagement === true;
     }

And in hooks/common/useSidebar.js (supporting change):

-  const loadSidebarConfig = async () => {
+  const loadSidebarConfig = async () => {
+    let fromServer = false;
     try {
@@
-      if (res.data.success && res.data.data.sidebar_config) {
+      if (res.data.success && res.data.data.sidebar_config) {
         setSidebarConfig(res.data.data.sidebar_config);
+        fromServer = true;
       } else {
         setSidebarConfig(DEFAULT_SYSTEM_SIDEBAR_CONFIG);
       }
     } catch (error) {
       setSidebarConfig(DEFAULT_SYSTEM_SIDEBAR_CONFIG);
     } finally {
       setLoading(false);
     }
+    return fromServer;
   };
@@
-  const refreshUserConfig = async () => {
-    await loadSidebarConfig();
+  const [hasServerConfig, setHasServerConfig] = useState(false);
+  const refreshUserConfig = async () => {
+    const fromServer = await loadSidebarConfig();
+    setHasServerConfig(fromServer);
     sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
   };
@@
   return {
     loading,
     sidebarConfig,
     finalConfig,
+    hasServerConfig,
     isModuleVisible,
     hasSectionVisibleModules,
     getVisibleModules,
     refreshUserConfig,
   };
web/src/hooks/common/useSidebar.js (1)

20-29: Prevent self-triggered double reloads; make event target SSR-safe; add stable instanceId

Emit a sourceId on dispatch and ignore self in the listener. Also guard window for SSR. This avoids double load after refreshUserConfig and prevents ReferenceError in non-browser contexts.

- import { useState, useEffect } from 'react';
+ import { useState, useEffect, useRef } from 'react';
-// 创建一个全局事件系统来同步所有useSidebar实例
-if (!window.sidebarEventTarget) {
-  window.sidebarEventTarget = new EventTarget();
-}
-const sidebarEventTarget = window.sidebarEventTarget;
-const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
+// 创建一个全局事件系统来同步所有useSidebar实例(SSR安全)
+const isBrowser = typeof window !== 'undefined';
+const sidebarEventTarget = isBrowser
+  ? (window.sidebarEventTarget ?? (window.sidebarEventTarget = new EventTarget()))
+  : new EventTarget();
+const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
 export const useSidebar = () => {
-  const [sidebarConfig, setSidebarConfig] = useState(null);
+  const [sidebarConfig, setSidebarConfig] = useState(null);
+  const instanceId = useRef(Symbol('useSidebarInstance'));
   const refreshUserConfig = async () => {
     await loadSidebarConfig();
     // 触发全局刷新事件,通知所有useSidebar实例更新
-    sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
+    sidebarEventTarget.dispatchEvent(
+      new CustomEvent(SIDEBAR_REFRESH_EVENT, { detail: { sourceId: instanceId.current } })
+    );
   };
-  useEffect(() => {
-    const handleRefresh = () => {
-      loadSidebarConfig();
-    };
+  useEffect(() => {
+    const handleRefresh = (e) => {
+      if (e?.detail?.sourceId === instanceId.current) return;
+      loadSidebarConfig();
+    };

Also applies to: 30-33, 86-91, 98-109

web/src/components/table/users/modals/EditUserGroupModal.jsx (3)

1-17: License header inconsistent with repo (use AGPL v3 and “QuantumNous”)

Replace Apache-2.0 header with the project-standard AGPL v3 header and correct owner name.

-/*
-Copyright 2024 Quantumnous Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-For commercial licensing, please contact support@quantumnous.com
-*/
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/

99-103: 0 ratio becomes 1.0 due to || fallback

Use nullish coalescing so 0 is preserved.

-  const getInitValues = () => ({
+  const getInitValues = () => ({
     name: editingGroup?.name || '',
     description: editingGroup?.description || '',
-    ratio: editingGroup?.ratio || 1.0,
+    ratio: editingGroup?.ratio ?? 1.0,
   });

115-118: Parsing ratio with || also converts 0 to 1.0

Parse and only default when NaN.

-      const data = {
-        ...values,
-        ratio: parseFloat(values.ratio) || 1.0,
-      };
+      const parsed =
+        typeof values.ratio === 'number' ? values.ratio : parseFloat(values.ratio);
+      const data = {
+        ...values,
+        ratio: Number.isFinite(parsed) ? parsed : 1.0,
+      };
web/src/components/table/users/modals/UserGroupManagement.jsx (1)

1-17: License header mismatch (Apache vs AGPL) and company name typo

🧹 Nitpick comments (38)
web/src/components/settings/OtherSetting.jsx (1)

109-113: DRY and handle empty value when updating title

Mirror the same empty-value handling here to keep behavior consistent with setStatusData, and consider extracting a tiny helper (e.g., updateSystemName(name)) to avoid duplication.

-      // 更新localStorage并触发title更新事件
-      localStorage.setItem('system_name', inputs.SystemName);
-      window.dispatchEvent(new CustomEvent('systemNameUpdated', {
-        detail: { systemName: inputs.SystemName }
-      }));
+      // 同步标题(支持清空还原默认标题)
+      const name = (inputs.SystemName ?? '').trim();
+      if (name) {
+        localStorage.setItem('system_name', name);
+      } else {
+        localStorage.removeItem('system_name');
+      }
+      window.dispatchEvent(new CustomEvent('systemNameUpdated', {
+        detail: { systemName: name }
+      }));
model/main.go (2)

117-118: Remove stray blank lines for consistency.
Two consecutive empty lines snuck in; trim to match existing file style.


276-280: Seed defaults in all migration paths; consider idempotent per-name seeding.
Good call seeding after AutoMigrate. Two follow-ups:

  • migrateDBFast path doesn’t seed; add the same call there after successful migrations.
  • Make seeding idempotent per group name (create missing defaults individually), not just “if count==0”.

Example addition to migrateDBFast (after checking errChan and before logging “database migrated”):

if err := InitDefaultUserGroups(); err != nil {
    common.SysLog("初始化默认用户分组失败: " + err.Error())
}

And consider adjusting InitDefaultUserGroups to FirstOrCreate each of: default, vip, svip.

web/src/hooks/common/useUserPermissions.js (1)

40-48: Prefer a debug logger over commented-out console statements

Replace commented logs with a debug-level logger (e.g., console.debug guarded by NODE_ENV or a central logger) to keep observability in dev without polluting prod.

-        //console.log('用户权限加载成功:', userPermissions);
+        if (process.env.NODE_ENV !== 'production') {
+          console.debug('用户权限加载成功:', userPermissions);
+        }
...
-        //console.error('获取权限失败:', res.data.message);
+        if (process.env.NODE_ENV !== 'production') {
+          console.debug('获取权限失败:', res.data.message);
+        }
...
-      //console.error('加载用户权限异常:', error);
+      if (process.env.NODE_ENV !== 'production') {
+        console.debug('加载用户权限异常:', error);
+      }
web/src/hooks/common/useHeaderBar.js (1)

55-55: Be tolerant to mixed backends; fall back to legacy field name

During rolling deploys, read either header_nav_modules or HeaderNavModules to avoid empty menus when versions are skewed.

-  const headerNavModulesConfig = statusState?.status?.header_nav_modules;
+  const headerNavModulesConfig =
+    statusState?.status?.header_nav_modules ??
+    statusState?.status?.HeaderNavModules;
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)

131-136: Also accept new option key when loading

Admin options currently read only props.options.HeaderNavModules. If backend migrates to header_nav_modules, the UI will miss it. Read both, preferring the new one.

-    if (props.options && props.options.HeaderNavModules) {
+    const raw =
+      props.options?.header_nav_modules ?? props.options?.HeaderNavModules;
+    if (raw) {
       try {
-        const modules = JSON.parse(props.options.HeaderNavModules);
+        const modules = JSON.parse(raw);
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)

93-117: Use stable defaults when merging options to avoid clobbering user edits

currentInputs copies from the mutable inputs state. If a user tweaks the form before options load, their edits become the new “defaults.” Start from a stable DEFAULT_INPUTS or a ref captured at mount.

-    // 从初始 inputs 开始,确保保留所有默认值
-    const currentInputs = { ...inputs };
+    // 从稳定的默认值开始,确保保留所有默认值
+    const currentInputs = { ...DEFAULT_INPUTS };

Add near the top (outside the component or via useRef):

// Outside component (preferred)
const DEFAULT_INPUTS = {
  TopUpLink: '',
  'general_setting.docs_link': '',
  QuotaPerUnit: '',
  RetryTimes: '',
  USDExchangeRate: '',
  DisplayInCurrencyEnabled: false,
  DisplayTokenStatEnabled: false,
  DefaultCollapseSidebar: false,
  DemoSiteEnabled: false,
  SelfUseModeEnabled: false,
  'general_setting.invitation_enabled': true,
};

Or inside component:

+  const defaultInputsRef = useRef(DEFAULT_INPUTS);
...
-    const currentInputs = { ...inputs };
+    const currentInputs = { ...defaultInputsRef.current };
web/src/components/topup/index.jsx (1)

328-334: Avoid extra network calls: prefer StatusContext first

If statusState.status.invitation_enabled is defined, use it and skip /api/status; only fetch when undefined.

-    getInvitationConfig().then();
+    const val = statusState?.status?.invitation_enabled;
+    if (typeof val === 'boolean') {
+      const enabled = val === true;
+      setInvitationEnabled(enabled);
+      setInvitationConfigLoaded(true);
+      if (enabled && !affFetchedRef.current) {
+        affFetchedRef.current = true;
+        getAffLink();
+      }
+    } else {
+      getInvitationConfig().then();
+    }
web/src/components/auth/ModuleRoute.jsx (3)

27-31: Unauthenticated should redirect to /login, not /forbidden.

Better UX: send unauthenticated users to login while keeping forbidden for authorized-but-no-permission cases.

I can provide a patch to track authRequired and render when no user is found. Want me to draft it?

Also applies to: 167-175


20-23: Avoid setState on unmounted during async checks.

checkModulePermission is async and can resolve after unmount.

Add a mounted flag or AbortController in useEffect to guard setHasPermission calls.

Also applies to: 24-57, 167-171


118-132: Minor: reduce duplicate API calls and checks.

  • console.detail allowlist is checked in two places.
  • /api/user/self is fetched multiple times per guard evaluation.

Consider memoizing self data in context or passing it down to reduce requests and duplicate checks.

Also applies to: 140-165

web/src/App.jsx (1)

332-339: Gate chat route with authentication for consistency.

Other console pages use PrivateRoute. This one only uses ModuleRoute, which returns /forbidden when not logged in. Prefer consistent auth gating.

Apply this diff:

-          element={
-            <ModuleRoute modulePath="chat.chat">
-              <Suspense fallback={<Loading></Loading>} key={location.pathname}>
-                <Chat />
-              </Suspense>
-            </ModuleRoute>
-          }
+          element={
+            <PrivateRoute>
+              <ModuleRoute modulePath="chat.chat">
+                <Suspense fallback={<Loading></Loading>} key={location.pathname}>
+                  <Chat />
+                </Suspense>
+              </ModuleRoute>
+            </PrivateRoute>
+          }
web/src/i18n/locales/en.json (1)

2127-2127: Capitalization consistency.

"default" → "Default" to match other labels (e.g., "Default" at Line 516).

Apply this diff:

-  "默认": "default",
+  "默认": "Default",
model/user.go (1)

408-417: Avoid second query by username; use assigned ID and handle update error

Use the inserted user's ID to reload (safer, avoids relying on unique username) and check Update() error.

Apply:

-  var createdUser User
-  if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil {
+  var createdUser User
+  if err := DB.First(&createdUser, user.Id).Error; err == nil {
     // 生成基于角色的默认边栏配置
     defaultSidebarConfig := GenerateDefaultSidebarConfigForRole(createdUser.Role)
     if defaultSidebarConfig != "" {
       currentSetting := createdUser.GetSetting()
       currentSetting.SidebarModules = defaultSidebarConfig
       createdUser.SetSetting(currentSetting)
-      createdUser.Update(false)
+      if err := createdUser.Update(false); err != nil {
+        common.SysLog("初始化边栏配置失败: " + err.Error())
+      }
       common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
     }
   }
web/src/components/settings/personal/cards/NotificationSettings.jsx (2)

173-204: Deduplicate default system config

This default is duplicated across files; centralize in useSidebar to avoid drift.

Apply in this file:

-import { useSidebar } from '../../../../hooks/common/useSidebar';
+import { useSidebar, DEFAULT_SYSTEM_SIDEBAR_CONFIG } from '../../../../hooks/common/useSidebar';
@@
-// 获取默认系统配置
-const getDefaultSystemConfig = () => {
-  return {
-    chat: { enabled: true, playground: true, chat: true },
-    console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
-    personal: { enabled: true, topup: true, personal: true },
-    admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true }
-  };
-};
+// 统一从hook导出的常量获取默认系统配置
+const getDefaultSystemConfig = () => DEFAULT_SYSTEM_SIDEBAR_CONFIG;

And in hooks/common/useSidebar.js (supporting change):

 export const useSidebar = () => {
@@
-  const defaultSidebarConfig = {
+  export const DEFAULT_SYSTEM_SIDEBAR_CONFIG = {
     chat: { enabled: true, playground: true, chat: true },
     console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
     personal: { enabled: true, topup: true, personal: true },
     admin: {
       enabled: true,
       channel: true,
       models: true,
       redemption: true,
       user: { enabled: true, groupManagement: true },
       setting: true
     }
   };
@@
-      if (res.data.success && res.data.data.sidebar_config) {
-        setSidebarConfig(res.data.data.sidebar_config);
+      if (res.data.success && res.data.data.sidebar_config) {
+        setSidebarConfig(res.data.data.sidebar_config);
       } else {
-        setSidebarConfig(defaultSidebarConfig);
+        setSidebarConfig(DEFAULT_SYSTEM_SIDEBAR_CONFIG);
       }
     } catch (error) {
-      setSidebarConfig(defaultSidebarConfig);
+      setSidebarConfig(DEFAULT_SYSTEM_SIDEBAR_CONFIG);
     } finally {

209-247: Be defensive when parsing setting; handle non-string objects

Avoid JSON.parse on an object; fallback stays intact.

Apply:

-          if (userRes.data.data.setting) {
+          if (userRes.data.data.setting) {
             try {
-              const setting = JSON.parse(userRes.data.data.setting);
+              const raw = userRes.data.data.setting;
+              const setting = typeof raw === 'string' ? JSON.parse(raw) : raw;
@@
-              const systemConfig = setting.sidebar_system_config ?
-                JSON.parse(setting.sidebar_system_config) :
-                getDefaultSystemConfig();
+              const systemConfig = setting.sidebar_system_config
+                ? (typeof setting.sidebar_system_config === 'string'
+                    ? JSON.parse(setting.sidebar_system_config)
+                    : setting.sidebar_system_config)
+                : getDefaultSystemConfig();
web/src/components/table/users/UsersActions.jsx (2)

29-37: Avoid trusting localStorage for role checks

Rely solely on server-derived finalConfig/permissions to prevent client-side spoofing; root can be inferred from finalConfig if needed.

Apply:

-  const getUserRole = () => {
-    const user = JSON.parse(localStorage.getItem('user') || '{}');
-    return user?.role || 0;
-  };
-
-  const isAdmin = () => getUserRole() >= 10;
-  const isRoot = () => getUserRole() >= 100;
+  // Prefer基于后端计算的权限:将root特权编码到finalConfig中,或使用单独的useUserPermissions钩子
+  const isAdmin = () => Boolean(finalConfig?.admin?.enabled);
+  const isRoot = () => Boolean(finalConfig?.admin?.setting === true);

96-103: Minor: compute once per render

Avoid double invocation of canShowGroupManagement in JSX.

Apply:

-    <>
-      <div className='flex gap-2 w-full md:w-auto order-2 md:order-1'>
+    <>
+      {/** 预先计算权限,避免重复计算 */}
+      {(() => { const showGroup = canShowGroupManagement(); return (
+      <div className='flex gap-2 w-full md:w-auto order-2 md:order-1'>
         <Button className='w/full md:w-auto' onClick={handleAddUser} size='small'>
           {t('添加用户')}
         </Button>
-        {canShowGroupManagement() && (
+        {showGroup && (
           <Button
             className='w-full md:w-auto'
             onClick={handleGroupManagement}
             size='small'
             theme='light'
           >
             {t('分组管理')}
           </Button>
         )}
       </div>
-
-      {canShowGroupManagement() && (
+      {showGroup && (
         <UserGroupManagement
           visible={showGroupManagement}
           onClose={() => setShowGroupManagement(false)}
           onGroupUpdated={onRefreshUsers}
         />
       )}
-    </>
+      )})()}
+    </>
middleware/auth.go (2)

241-255: Avoid permission list drift between backend gate and status filtering

isUserModuleAllowed duplicates the allowlist in controller/misc.go:isUserModuleAllowedInFilter. Extract a single shared allowlist/utility to prevent divergence.


277-283: Log JSON parse failures for easier ops troubleshooting

You log failures here via SysLog—good. Consider doing the same in controller filters for symmetry.

router/api-router.go (2)

176-183: Admin log routes gated by console.log may 403 when SidebarModulesAdmin exists

Given current isAdminModuleAllowed/checkNestedPermission, admin users can be denied if console.log isn’t present in SidebarModulesAdmin. Either:

  • rely on the middleware fix to allow console.* for admins, or
  • change these to an admin.* module key that’s present in config.

207-215: New admin.user.groupManagement path requires config rollout coordination

If SidebarModulesAdmin exists without this nested key, admins will be denied until config is updated. Ensure defaults or migrations seed this path enabled, or rely on the “unspecified=allow” middleware change.

I can draft a migration to add admin.user.groupManagement: true into SidebarModulesAdmin.

controller/misc.go (3)

334-341: Add logging on JSON parse error for header nav config

Silent "{}" makes ops debugging harder.

-	if err := json.Unmarshal([]byte(headerNavModulesStr), &config); err != nil {
-		// 解析失败时返回空配置,采用安全优先策略
-		return "{}"
-	}
+	if err := json.Unmarshal([]byte(headerNavModulesStr), &config); err != nil {
+		// 解析失败时返回空配置,采用安全优先策略
+		common.SysLog("解析 HeaderNavModules 失败: " + err.Error())
+		return "{}"
+	}

384-391: Also log parse failures for SidebarModulesAdmin filter

Symmetry with middleware logging; aids support.

-	if err := json.Unmarshal([]byte(sidebarModulesStr), &config); err != nil {
-		// 解析失败时返回空配置,采用安全优先策略
-		return "{}"
-	}
+	if err := json.Unmarshal([]byte(sidebarModulesStr), &config); err != nil {
+		// 解析失败时返回空配置,采用安全优先策略
+		common.SysLog("解析 SidebarModulesAdmin 失败: " + err.Error())
+		return "{}"
+	}

556-579: DRY: Duplicate allowlists with middleware; centralize to avoid drift

isUserModuleAllowedInFilter should share the same source of truth as middleware.isUserModuleAllowed.

web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (3)

169-173: Include detail in CustomEvent for consistent payload shape

Add a sourceId so listeners that ignore self-originated events keep working consistently across emitters.

-        if (window.sidebarEventTarget) {
-          window.sidebarEventTarget.dispatchEvent(new CustomEvent('sidebar-refresh'));
-        }
+        if (window.sidebarEventTarget) {
+          window.sidebarEventTarget.dispatchEvent(
+            new CustomEvent('sidebar-refresh', { detail: { sourceId: 'settings-admin' } })
+          );
+        }

415-467: Simplify conditional rendering and disabled logic for “分组管理”

The nested ternaries redundantly re-check module.key === 'user'. Simplify for readability.

-                    {/* 为用户管理添加分组管理子开关 */}
-                    {module.key === 'user' && (
-                      module.key === 'user'
-                        ? sidebarModulesAdmin[section.key]?.user?.enabled
-                        : sidebarModulesAdmin[section.key]?.[module.key]
-                    ) && (
+                    {/* 为用户管理添加分组管理子开关 */}
+                    {module.key === 'user' && sidebarModulesAdmin[section.key]?.user?.enabled && (
                       <div style={{
                         borderTop: '1px solid var(--semi-color-border)',
                         marginTop: '12px',
                         paddingTop: '12px'
                       }}>
                         <div style={{
                           display: 'flex',
                           justifyContent: 'space-between',
                           alignItems: 'center'
                         }}>
                           <div style={{ flex: 1, textAlign: 'left' }}>
                             <div style={{
                               fontWeight: '500',
                               fontSize: '12px',
                               color: 'var(--semi-color-text-1)',
                               marginBottom: '2px'
                             }}>
                               {t('分组管理')}
                             </div>
                             <Text
                               type="secondary"
                               size="small"
                               style={{
                                 fontSize: '11px',
                                 color: 'var(--semi-color-text-2)',
                                 lineHeight: '1.4',
                                 display: 'block'
                               }}
                             >
                               {t('控制管理员是否可以访问分组管理功能')}
                             </Text>
                           </div>
                           <div style={{ marginLeft: '16px' }}>
                             <Switch
-                              checked={sidebarModulesAdmin[section.key]?.user?.groupManagement || false}
+                              checked={!!sidebarModulesAdmin[section.key]?.user?.groupManagement}
                               onChange={handleUserGroupManagementChange}
                               size="small"
-                              disabled={!sidebarModulesAdmin[section.key]?.enabled || !(
-                                module.key === 'user'
-                                  ? sidebarModulesAdmin[section.key]?.user?.enabled
-                                  : sidebarModulesAdmin[section.key]?.[module.key]
-                              )}
+                              disabled={
+                                !sidebarModulesAdmin[section.key]?.enabled ||
+                                !sidebarModulesAdmin[section.key]?.user?.enabled
+                              }
                             />
                           </div>
                         </div>
                       </div>
                     )}

38-41: DRY the default modules object to avoid divergence

The same default structure appears 3 times. Extract a single DEFAULT_MODULES constant and reuse to reduce drift and maintenance.

   const { t } = useTranslation();
   const [loading, setLoading] = useState(false);
-  
+  const DEFAULT_MODULES = {
+    chat: { enabled: true, playground: true, chat: true },
+    console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
+    personal: { enabled: true, topup: true, personal: true },
+    admin: {
+      enabled: true,
+      channel: true,
+      models: true,
+      redemption: true,
+      user: { enabled: true, groupManagement: false },
+      setting: true,
+    },
+  };
-  const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({
-    chat: {
-      enabled: true,
-      playground: true,
-      chat: true,
-    },
-    console: {
-      enabled: true,
-      detail: true,
-      token: true,
-      log: true,
-      midjourney: true,
-      task: true,
-    },
-    personal: {
-      enabled: true,
-      topup: true,
-      personal: true,
-    },
-    admin: {
-      enabled: true,
-      channel: true,
-      models: true,
-      redemption: true,
-      user: {
-        enabled: true,
-        groupManagement: false // 默认关闭分组管理
-      },
-      setting: true
-    }
-  });
+  const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState(DEFAULT_MODULES);
-  function resetSidebarModules() {
-    const defaultModules = {
-      chat: {
-        enabled: true,
-        playground: true,
-        chat: true,
-      },
-      console: {
-        enabled: true,
-        detail: true,
-        token: true,
-        log: true,
-        midjourney: true,
-        task: true,
-      },
-      personal: {
-        enabled: true,
-        topup: true,
-        personal: true,
-      },
-      admin: {
-        enabled: true,
-        channel: true,
-        models: true,
-        redemption: true,
-        user: {
-          enabled: true,
-          groupManagement: false // 默认关闭分组管理
-        },
-        setting: true
-      }
-    };
-    setSidebarModulesAdmin(defaultModules);
+  function resetSidebarModules() {
+    setSidebarModulesAdmin(DEFAULT_MODULES);
     showSuccess(t('已重置为默认配置'));
   }
-        // 使用默认配置
-        const defaultModules = {
-          chat: { enabled: true, playground: true, chat: true },
-          console: {
-            enabled: true,
-            detail: true,
-            token: true,
-            log: true,
-            midjourney: true,
-            task: true,
-          },
-          personal: { enabled: true, topup: true, personal: true },
-          admin: {
-            enabled: true,
-            channel: true,
-            models: true,
-            redemption: true,
-            user: {
-              enabled: true,
-              groupManagement: false // 默认关闭分组管理
-            },
-            setting: true
-          }
-        };
-        setSidebarModulesAdmin(defaultModules);
+        // 使用默认配置
+        setSidebarModulesAdmin(DEFAULT_MODULES);

Also applies to: 40-70, 115-150, 192-214

web/src/components/table/users/modals/EditUserGroupModal.jsx (2)

56-60: Avoid denying while permissions are still loading

Currently hasGroupManagementPermission returns false when sidebarLoading, causing a misleading “无权访问” on submit. Gate submit until config is loaded or show a friendly retry message.

-  const hasGroupManagementPermission = () => {
-    // 如果侧边栏配置还在加载中,暂时拒绝访问
-    if (sidebarLoading) {
-      return false;
-    }
+  const hasGroupManagementPermission = () => {
+    if (sidebarLoading) return false; // 由提交流程提示重试
-    // 检查权限
-    if (!hasGroupManagementPermission()) {
-      showError(t('无权访问分组管理功能'));
+    // 等待权限加载完成
+    if (sidebarLoading) {
+      showError(t('权限加载中,请稍后重试'));
+      onClose();
+      return;
+    }
+    // 检查权限
+    if (!hasGroupManagementPermission()) {
+      showError(t('无权访问分组管理功能'));
       onClose();
       return;
     }

Also applies to: 105-112


202-206: Show spinner while either local submit or sidebar permissions are loading

Improves UX during permission fetch.

-      <Spin spinning={loading}>
+      <Spin spinning={loading || sidebarLoading}>
web/src/components/table/users/modals/UserGroupManagement.jsx (1)

191-204: Consider using the shared getGroupDescription helper.

This component duplicates the getGroupDescription logic from web/src/helpers/render.jsx. Using the shared implementation would ensure consistency.

Import and use the shared helper:

+import { getGroupDescription } from '../../../../helpers/render';

-  // 获取分组描述的翻译
-  const getGroupDescription = (groupName, originalDescription) => {
-    // 对于系统默认分组,使用翻译
-    if (groupName === 'default' && originalDescription === '默认分组') {
-      return t('默认分组');
-    }
-    if (groupName === 'vip' && originalDescription === 'VIP分组') {
-      return t('VIP分组');
-    }
-    if (groupName === 'svip' && originalDescription === 'SVIP分组') {
-      return t('SVIP分组');
-    }
-    // 对于用户自定义分组,使用原始描述
-    return originalDescription;
-  };
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)

108-154: Consider extracting complex config merging logic.

The mergeUserConfigWithSystemConfig function is quite complex with nested conditionals. Consider breaking it down for better maintainability.

Extract the module processing logic:

const processModuleConfig = (systemModuleValue, userModuleValue, defaultEnabled = true) => {
  // Handle boolean module
  if (typeof systemModuleValue === 'boolean' && systemModuleValue === true) {
    return userModuleValue !== undefined ? userModuleValue : defaultEnabled;
  }
  
  // Handle nested object module
  if (typeof systemModuleValue === 'object' && systemModuleValue !== null && systemModuleValue.enabled === true) {
    if (userModuleValue !== undefined) {
      if (typeof userModuleValue === 'boolean') {
        return userModuleValue;
      } else if (typeof userModuleValue === 'object' && userModuleValue !== null) {
        return userModuleValue.enabled !== false;
      }
    }
    return systemModuleValue.enabled;
  }
  
  return false;
};

const mergeUserConfigWithSystemConfig = (userConfig, systemConfig) => {
  const mergedConfig = {};
  
  Object.keys(systemConfig).forEach(sectionKey => {
    const systemSection = systemConfig[sectionKey];
    if (!systemSection?.enabled) return;
    
    const userSection = userConfig[sectionKey] || {};
    mergedConfig[sectionKey] = {
      enabled: userSection.enabled !== undefined ? userSection.enabled : true
    };
    
    Object.keys(systemSection).forEach(moduleKey => {
      if (moduleKey !== 'enabled') {
        mergedConfig[sectionKey][moduleKey] = processModuleConfig(
          systemSection[moduleKey],
          userSection[moduleKey],
          true
        );
      }
    });
  });
  
  return mergedConfig;
};
controller/user.go (1)

758-889: Complex nested logic in cleanUserSettingForResponse needs simplification.

This function has multiple levels of nesting and tries to do too many things: parsing, filtering, extending, and serializing settings. This violates the single responsibility principle.

Split into focused functions:

func cleanUserSettingForResponse(originalSetting string, systemSidebarConfig map[string]interface{}) string {
    userSetting, err := parseUserSetting(originalSetting)
    if err != nil {
        return originalSetting
    }
    
    if userSetting.SidebarModules == "" {
        return originalSetting
    }
    
    filteredModules := filterSidebarModules(userSetting.SidebarModules, systemSidebarConfig)
    if filteredModules == nil {
        return originalSetting
    }
    
    extendedSetting := buildExtendedSetting(originalSetting, filteredModules, systemSidebarConfig)
    
    result, err := json.Marshal(extendedSetting)
    if err != nil {
        return originalSetting
    }
    
    return string(result)
}

func parseUserSetting(originalSetting string) (*dto.UserSetting, error) {
    if originalSetting == "" {
        return nil, fmt.Errorf("empty setting")
    }
    
    var userSetting dto.UserSetting
    if err := json.Unmarshal([]byte(originalSetting), &userSetting); err != nil {
        return nil, err
    }
    return &userSetting, nil
}

func filterSidebarModules(sidebarModulesJSON string, systemConfig map[string]interface{}) map[string]interface{} {
    // Extract filtering logic here
    // ...
}

func buildExtendedSetting(originalSetting string, filteredModules map[string]interface{}, systemConfig map[string]interface{}) map[string]interface{} {
    // Build extended setting object
    // ...
}
web/src/components/layout/PageLayout.jsx (1)

151-156: Unify header height as a single source of truth.

Hardcoded 64px appears here and elsewhere. Use a CSS variable to prevent drift if header height changes.

Example (outside this hunk):

  • Define once in a global stylesheet: :root { --header-height: 64px; }
  • Then here:
top: 'var(--header-height)',
height: 'calc(100vh - var(--header-height))',

Do the same for any other 64px header offsets.

web/src/pages/Home/index.jsx (4)

159-166: Prevent flex child overflow: add min-h-0 to scrollable wrappers.

In flex columns, parents default to min-height:auto, which can block children from scrolling. Add min-h-0 to the immediate flex-1 wrappers.

-        <div className='w-full overflow-x-hidden flex-1 flex flex-col'>
+        <div className='w-full overflow-x-hidden flex-1 flex flex-col min-h-0'>

Also consider min-h-0 on other intermediate flex containers if any child needs overflow/scroll.


346-351: Same flex fix for the “content is URL” branch.

Mirror min-h-0 here so the iframe can size/scroll correctly inside the flex column.

-        <div className='overflow-x-hidden w-full flex-1 flex flex-col'>
+        <div className='overflow-x-hidden w-full flex-1 flex flex-col min-h-0'>
...
-            <iframe
+            <iframe
               src={homePageContent}
               className='w-full flex-1 border-none'
             />

355-355: Replace magic 60px header offset with a shared variable.

The header offset here (60px) differs from the 64px used elsewhere, causing visual drift.

-              className='mt-[60px] flex-1'
+              className='mt-[var(--header-height,64px)] flex-1'

Confirm across pages that all header offsets reference the same variable.


236-246: Harden external window.open calls to avoid reverse tabnabbing.

When opening external sites, clear opener.

Example:

const w = window.open('https://github.com/QuantumNous/new-api', '_blank', 'noopener,noreferrer');
if (w) w.opener = null;
📜 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 d798db5 and ed5d9ea.

📒 Files selected for processing (43)
  • controller/misc.go (4 hunks)
  • controller/user.go (13 hunks)
  • controller/user_group.go (1 hunks)
  • middleware/auth.go (2 hunks)
  • model/main.go (3 hunks)
  • model/option.go (1 hunks)
  • model/user.go (7 hunks)
  • model/user_group.go (1 hunks)
  • router/api-router.go (7 hunks)
  • setting/operation_setting/general_setting.go (1 hunks)
  • web/index.html (1 hunks)
  • web/src/App.jsx (6 hunks)
  • web/src/components/auth/AuthPageLayout.jsx (1 hunks)
  • web/src/components/auth/LoginForm.jsx (3 hunks)
  • web/src/components/auth/ModuleRoute.jsx (1 hunks)
  • web/src/components/auth/PasswordResetConfirm.jsx (3 hunks)
  • web/src/components/auth/PasswordResetForm.jsx (3 hunks)
  • web/src/components/auth/RegisterForm.jsx (3 hunks)
  • web/src/components/layout/Footer.jsx (1 hunks)
  • web/src/components/layout/PageLayout.jsx (1 hunks)
  • web/src/components/layout/headerbar/LanguageSelector.jsx (1 hunks)
  • web/src/components/settings/OtherSetting.jsx (1 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (6 hunks)
  • web/src/components/settings/personal/components/UserInfoHeader.jsx (2 hunks)
  • web/src/components/table/users/UsersActions.jsx (1 hunks)
  • web/src/components/table/users/index.jsx (1 hunks)
  • web/src/components/table/users/modals/AddUserModal.jsx (4 hunks)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
  • web/src/components/topup/index.jsx (6 hunks)
  • web/src/helpers/api.js (2 hunks)
  • web/src/helpers/data.js (1 hunks)
  • web/src/helpers/render.jsx (2 hunks)
  • web/src/helpers/utils.jsx (1 hunks)
  • web/src/hooks/common/useHeaderBar.js (1 hunks)
  • web/src/hooks/common/useSidebar.js (3 hunks)
  • web/src/hooks/common/useUserPermissions.js (1 hunks)
  • web/src/i18n/locales/en.json (3 hunks)
  • web/src/pages/Home/index.jsx (2 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (3 hunks)
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1 hunks)
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (8 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • web/src/components/layout/headerbar/LanguageSelector.jsx
🚧 Files skipped from review as they are similar to previous changes (16)
  • model/option.go
  • web/src/components/table/users/index.jsx
  • web/src/components/layout/Footer.jsx
  • setting/operation_setting/general_setting.go
  • web/src/helpers/utils.jsx
  • web/src/components/auth/RegisterForm.jsx
  • web/src/components/settings/personal/components/UserInfoHeader.jsx
  • web/src/helpers/render.jsx
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/auth/AuthPageLayout.jsx
  • web/src/components/table/users/modals/AddUserModal.jsx
  • web/src/components/auth/PasswordResetForm.jsx
  • web/src/components/auth/PasswordResetConfirm.jsx
  • web/src/helpers/api.js
  • controller/user_group.go
  • model/user_group.go
🧰 Additional context used
🧬 Code graph analysis (20)
web/src/components/settings/OtherSetting.jsx (1)
common/custom-event.go (1)
  • CustomEvent (51-58)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
web/src/hooks/common/useHeaderBar.js (1)
  • headerNavModules (58-78)
web/src/hooks/common/useHeaderBar.js (5)
web/src/App.jsx (1)
  • statusState (58-58)
web/src/components/auth/ModuleRoute.jsx (1)
  • statusState (18-18)
web/src/components/settings/OtherSetting.jsx (1)
  • statusState (49-49)
web/src/pages/Home/index.jsx (1)
  • statusState (70-70)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
  • statusState (39-39)
web/src/App.jsx (3)
web/src/hooks/common/useHeaderBar.js (3)
  • headerNavModulesConfig (55-55)
  • statusState (34-34)
  • location (40-40)
web/src/components/auth/ModuleRoute.jsx (2)
  • statusState (18-18)
  • ModuleRoute (16-179)
web/src/helpers/auth.jsx (1)
  • AdminRoute (52-66)
model/main.go (2)
model/user_group.go (2)
  • UserGroup (8-16)
  • InitDefaultUserGroups (90-124)
common/sys_log.go (1)
  • SysLog (10-13)
middleware/auth.go (2)
common/constants.go (4)
  • RoleRootUser (136-136)
  • RoleAdminUser (135-135)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
common/sys_log.go (1)
  • SysLog (10-13)
web/src/components/topup/index.jsx (2)
web/src/components/topup/InvitationCard.jsx (1)
  • InvitationCard (34-227)
web/src/helpers/render.jsx (1)
  • renderQuota (907-925)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • handleModuleChange (116-127)
web/src/components/table/users/UsersActions.jsx (2)
web/src/components/table/users/modals/UserGroupManagement.jsx (5)
  • sidebarLoading (54-54)
  • getUserRole (57-60)
  • isAdmin (63-63)
  • isRoot (62-62)
  • UserGroupManagement (47-369)
web/src/hooks/common/useSidebar.js (3)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
web/src/components/table/users/modals/EditUserGroupModal.jsx (3)
web/src/hooks/common/useSidebar.js (4)
  • loading (32-32)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
web/src/helpers/utils.jsx (2)
  • showError (118-147)
  • showSuccess (153-155)
web/src/helpers/api.js (6)
  • res (245-245)
  • res (246-246)
  • res (289-289)
  • res (290-290)
  • API (30-38)
  • API (30-38)
controller/misc.go (2)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/constants.go (3)
  • OptionMap (36-36)
  • RoleRootUser (136-136)
  • RoleAdminUser (135-135)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (3)
web/src/components/settings/personal/cards/NotificationSettings.jsx (5)
  • permissionsLoading (91-97)
  • getDefaultSystemConfig (174-203)
  • adminConfig (88-88)
  • sidebarModulesUser (60-87)
  • handleModuleChange (116-127)
web/src/helpers/api.js (2)
  • API (30-38)
  • API (30-38)
web/src/hooks/common/useUserPermissions.js (1)
  • hasSidebarSettingsPermission (58-60)
web/src/components/auth/ModuleRoute.jsx (3)
web/src/App.jsx (1)
  • statusState (58-58)
web/src/hooks/common/useSidebar.js (2)
  • finalConfig (112-112)
  • sidebarConfig (31-31)
web/src/components/common/ui/Loading.jsx (1)
  • Loading (23-29)
model/user.go (4)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
model/log.go (1)
  • RecordLog (78-94)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
web/src/hooks/common/useSidebar.js (2)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • useSidebar (100-100)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
  • useSidebar (50-50)
web/src/components/table/users/modals/UserGroupManagement.jsx (4)
web/src/components/table/users/modals/EditUserGroupModal.jsx (9)
  • useTranslation (40-40)
  • loading (42-42)
  • sidebarLoading (43-43)
  • getUserRole (46-49)
  • isRoot (51-51)
  • isAdmin (52-52)
  • hasGroupManagementPermission (55-82)
  • getGroupDisplayName (92-97)
  • EditUserGroupModal (39-272)
web/src/hooks/common/useSidebar.js (4)
  • loading (32-32)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
web/src/helpers/render.jsx (1)
  • getGroupDisplayName (633-638)
web/src/components/common/ui/CardTable.jsx (1)
  • CardTable (42-232)
web/src/pages/Home/index.jsx (1)
web/src/components/layout/NoticeModal.jsx (1)
  • NoticeModal (39-253)
controller/user.go (6)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/json.go (2)
  • Unmarshal (8-10)
  • Marshal (20-22)
dto/user_settings.go (1)
  • UserSetting (3-13)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleAdminUser (135-135)
model/user.go (1)
  • GenerateDefaultSidebarConfigForRole (96-161)
common/sys_log.go (1)
  • SysLog (10-13)
web/src/components/settings/personal/cards/NotificationSettings.jsx (2)
web/src/hooks/common/useSidebar.js (3)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • refreshUserConfig (87-91)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)
  • useSidebar (50-50)
  • getDefaultSystemConfig (74-106)
router/api-router.go (3)
middleware/auth.go (3)
  • ModuleAuth (180-217)
  • AdminAuth (163-167)
  • UserAuth (157-161)
model/user_group.go (1)
  • GetAllUserGroups (38-42)
controller/user_group.go (4)
  • GetAllUserGroups (17-24)
  • CreateUserGroup (27-75)
  • UpdateUserGroup (78-153)
  • DeleteUserGroup (156-205)
🪛 Biome (2.1.2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

[error] 61-61: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


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

Hooks should not be called after an early return.

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] 218-218: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

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] 219-220: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

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] 298-298: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

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)

🔇 Additional comments (20)
model/main.go (1)

306-310: LGTM: included UserGroup in fast migrations.
This keeps fast path in sync with the primary migrate list.

web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)

112-115: Stringified header_nav_modules is correct: the set reducer replaces state.status with your payload (not wrapped) and useHeaderBar JSON.parses that string, so no changes are needed.

web/src/components/topup/index.jsx (2)

63-65: Good: initialize invitation gating to false to avoid UI flicker


519-567: Conditional layout logic looks good

Grid spans and conditional rendering are correctly gated by invitationConfigLoaded && invitationEnabled.

web/src/App.jsx (1)

60-81: header_nav_modules parsing looks good.

JSON parse guarded; sensible defaults; dependency array correct.

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

2085-2085: Fix English translation and verify Chinese key typo

  • In web/src/i18n/locales/en.json (line 2085), change the value to:

    "When disabled, invitation rewards are turned off"

  • In web/src/i18n/locales/zh.json, locate the key 关闭后:不在启用邀请奖励功能 (if present) and correct it to 关闭后:不再启用邀请奖励功能

model/user.go (1)

96-161: Default sidebar config generation looks sound

Covers expected sections and admin vs. root differences; structure matches frontend expectations (admin.user.enabled/groupManagement). No blockers.

web/src/components/settings/personal/cards/NotificationSettings.jsx (3)

99-101: Good: refresh global sidebar config after save

refreshUserConfig ensures immediate UI consistency. LGTM.


138-141: LGTM: trigger refresh post-save

Keeps other views in sync via the shared hook.


728-729: Copy/text change is fine

i18n string reads well.

router/api-router.go (1)

81-90: LGTM: per-module middleware placement is consistent and minimal

ModuleAuth is correctly stacked after role auth on relevant groups; paths align with module keys used by the frontend.

Also applies to: 112-139, 142-151, 165-174, 185-187, 218-220, 223-225, 239-241

controller/misc.go (2)

101-103: Good: status returns filtered header_nav_modules instead of raw config

Matches frontend expectations and reduces leakage of disabled items.


45-50: LGTM: role derivation and invitation_enabled exposure

Role defaulting to -1 is safe; invitation toggle wired to operation_setting.

Also applies to: 76-76

web/src/hooks/common/useSidebar.js (1)

59-64: Align default of admin.user.groupManagement with Admin panel defaults

Hook default is true but Admin UI defaults to false, causing inconsistent UX when backend lacks config or errors. Recommend defaulting to false here as well (or confirm product requirement).

-      user: {
-        enabled: true,
-        groupManagement: true  // 默认启用分组管理
-      },
+      user: {
+        enabled: true,
+        groupManagement: false
+      },
web/src/components/table/users/modals/EditUserGroupModal.jsx (1)

69-78: Backward compatibility when admin.user may be boolean

If older config stores admin.user as boolean, treat true as {enabled:true} and require groupManagement explicitly.

-      const userSection = finalConfig?.admin?.user;
+      const userSection = finalConfig?.admin?.user;
+      const userEnabled =
+        typeof userSection === 'object' ? userSection.enabled === true : userSection === true;
+      const groupMgmtEnabled =
+        typeof userSection === 'object' ? userSection.groupManagement === true : false;
 
-      // 检查用户管理模块是否启用
-      if (!userSection || userSection.enabled === false) {
+      // 检查用户管理模块是否启用
+      if (!userEnabled) {
         return false;
       }
 
-      // 检查分组管理子功能是否启用
-      return userSection.groupManagement === true;
+      // 检查分组管理子功能是否启用
+      return groupMgmtEnabled;
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)

463-496: Good responsive design and accessibility.

The grid layout with responsive breakpoints and hover states provides excellent UX across different screen sizes. The disabled state visual feedback when the section is disabled is also well implemented.

controller/user.go (3)

379-388: Good implementation of invitation feature toggle.

The consistent gating of invitation-related endpoints (TransferAffQuota and GetAffCode) with the centralized InvitationEnabled setting is well implemented and provides clear user feedback.

Also applies to: 415-424


1428-1431: Good default group assignment.

Setting the default group to "default" when not specified ensures data consistency and prevents potential null reference issues.


1540-1548: Excellent role transition handling with sidebar configuration sync.

The automatic synchronization of sidebar configurations when promoting/demoting users ensures consistent permissions and prevents privilege escalation issues. The logging provides good audit trail.

Also applies to: 1565-1573

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

152-152: LGTM: page wrapper aligns with full-height flex layout.

min-h-full + flex flex-col is consistent with the updated PageLayout behavior.

Comment thread controller/misc.go
Comment thread controller/user.go
Comment thread middleware/auth.go Outdated
Comment thread middleware/auth.go
Comment thread middleware/auth.go
Comment thread web/src/helpers/data.js Outdated
Comment thread web/src/i18n/locales/en.json Outdated
Comment thread web/src/i18n/locales/en.json
Comment thread web/src/pages/Setting/Operation/SettingsGeneral.jsx
Comment thread web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

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

Caution

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

⚠️ Outside diff range comments (1)
web/src/hooks/model-pricing/useModelPricingData.jsx (1)

165-177: Bug: group_ratio keyed by group, not by model; assignment is wrong

API returns group_ratio as map[groupName->ratio], but here it’s read via model_name, yielding undefined for most models. Either remove per-model assignment or map the selectedGroup ratio at render time.

Apply:

-      m.group_ratio = groupRatio[m.model_name];
♻️ Duplicate comments (13)
web/src/components/table/users/modals/UserGroupManagement.jsx (1)

55-94: DRY: extract permission logic to a shared hook.

Same role/permission checks appear in EditUserGroupModal.jsx. Centralize via a useGroupManagement hook as suggested previously.

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

2055-2055: Good copyedit.

“您可以个性化设置侧边栏要显示的功能” → “You can customize which features are shown in the sidebar” reads better.

web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)

46-47: Verify backend option key for invitation flag.

Confirm whether server expects “invitation_enabled” (top-level) vs “general_setting.invitation_enabled”. Align both read/submit keys accordingly and ensure boolean payload shape.

#!/bin/bash
# Inspect backend for the exact key path
rg -nP 'invitation_enabled' -C3 **/*.go

Also applies to: 235-245

controller/user_group.go (4)

44-61: Validation hardening in Create looks good.

Trim, empty-name rejection, reserved-name guard, and ratio normalization are correctly implemented.


101-116: Update path validation aligned with Create.

Name trim/empty check and ratio==0 normalization fix prior inconsistency.


318-320: Good: switched to thread-safe read for TopupGroupRatio.

Prevents “concurrent map iteration and map write” panics.


133-179: Atomic rename + user update wrapped in a transaction — nice.

This resolves prior partial-update risks on rename.

controller/user.go (1)

555-679: Consider extracting helpers to reduce nesting and improve testability.

Split section processing and nested-module handling into focused functions.

model/user_group.go (1)

26-37: Map-based Update preserves zero values — good.

This allows clearing fields like description and setting ratio precisely.

middleware/auth.go (2)

264-293: Allow admin access to all console. endpoints (not just console.detail)*

Currently only console.detail is whitelisted; other console.* paths get blocked by SidebarModulesAdmin. Delegate console.* to user allowlist.

 func isAdminModuleAllowed(modulePath string) bool {
   // 数据看板始终允许访问,不受控制台区域开关影响
   if modulePath == "console.detail" {
     return true
   }
+  // 管理员访问 console.* 功能沿用用户侧放行策略,避免被 SidebarModulesAdmin 误封
+  if strings.HasPrefix(modulePath, "console.") {
+    return isUserModuleAllowed(modulePath)
+  }
 
   // 获取侧边栏管理配置

296-343: Default-to-allow when config is missing/unspecified; only deny on explicit enabled:false

checkNestedPermission currently denies on missing keys/type mismatches, contradicting “unspecified => allow” and risks locking out new modules by default.

 func checkNestedPermission(config map[string]interface{}, modulePath string) bool {
   parts := strings.Split(modulePath, ".")
   current := config
 
   for i, part := range parts {
-    if current == nil {
-      return false
-    }
+    if current == nil {
+      return true
+    }
 
     value, exists := current[part]
-    if !exists {
-      return false
-    }
+    if !exists {
+      return true
+    }
 
     // 如果是最后一个部分,检查布尔值
     if i == len(parts)-1 {
       if boolVal, ok := value.(bool); ok {
         return boolVal
       }
       // 如果是对象且有enabled字段,检查enabled
       if objVal, ok := value.(map[string]interface{}); ok {
         if enabled, hasEnabled := objVal["enabled"]; hasEnabled {
           if enabledBool, ok := enabled.(bool); ok {
             return enabledBool
           }
         }
-        // 如果没有enabled字段,默认为true
+        // 如果没有enabled字段,默认为true
         return true
       }
-      return false
+      return true
     }
 
     // 中间路径必须是对象
     if objVal, ok := value.(map[string]interface{}); ok {
       // 检查区域是否启用
       if enabled, hasEnabled := objVal["enabled"]; hasEnabled {
         if enabledBool, ok := enabled.(bool); ok && !enabledBool {
           return false
         }
       }
       current = objVal
     } else {
-      return false
+      return true
     }
   }
 
-  return false
+  return true
 }
web/src/components/auth/ModuleRoute.jsx (1)

59-64: Critical: root access granted based on localStorage role.

This enables client-side privilege escalation by tampering localStorage. Do not grant ROOT from client-stored data.

Apply this minimal fix to stop trusting localStorage and rely on server-derived config:

-      const userRole = userObj.role;
-
-      // 使用精确角色匹配,避免范围检查导致的漂移
-      if (userRole === USER_ROLES.ROOT) {
-        return true; // 超级管理员始终有权限
-      }
+      // 不再基于本地角色直接授予 ROOT 权限,统一依赖服务端下发的最终配置
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)

42-47: Hooks order violation resolved — good fix.

Moving sidebarModulesUser, adminConfig, and configLoading to the top eliminates the prior Rules-of-Hooks error.

🧹 Nitpick comments (29)
web/src/helpers/data.js (3)

31-33: Guard event dispatch for SSR/non-browser contexts

Avoid ReferenceErrors when imported server-side.

-   window.dispatchEvent(new CustomEvent('systemNameUpdated', {
-     detail: { systemName: name }
-   }));
+   if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
+     window.dispatchEvent(new CustomEvent('systemNameUpdated', {
+       detail: { systemName: name }
+     }));
+   }

25-30: Avoid toString on non-strings; enforce string-only input

Prevents storing "false" or "[object Object]" if API sends non-strings.

-  const name = (data.system_name ?? '').toString().trim();
+  const raw = data.system_name;
+  const name = typeof raw === 'string' ? raw.trim() : '';

26-30: Harden against localStorage exceptions (quota/private mode)

Don’t break UI if storage writes fail; still emit the event.

-  if (name) {
-     localStorage.setItem('system_name', name);
-   } else {
-     localStorage.removeItem('system_name');
-   }
+  try {
+    if (name) {
+      localStorage.setItem('system_name', name);
+    } else {
+      localStorage.removeItem('system_name');
+    }
+  } catch (e) {
+    // ignore storage errors; event below will keep UI in sync
+  }
common/topup-ratio.go (1)

16-24: Name: consider Snapshot for clarity.

Optional: rename GetTopupGroupRatioCopy to GetTopupGroupRatioSnapshot for intent clarity. No functional change.

web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)

92-117: Merge from defaults, not current state, to avoid stale values.

Starting from current inputs can retain outdated values when options omit keys. Use a constant DEFAULT_INPUTS as the merge base.

-  useEffect(() => {
-    // 从初始 inputs 开始,确保保留所有默认值
-    const currentInputs = { ...inputs };
+  const DEFAULT_INPUTS = {
+    TopUpLink: '',
+    'general_setting.docs_link': '',
+    QuotaPerUnit: '',
+    RetryTimes: '',
+    USDExchangeRate: '',
+    DisplayInCurrencyEnabled: false,
+    DisplayTokenStatEnabled: false,
+    DefaultCollapseSidebar: false,
+    DemoSiteEnabled: false,
+    SelfUseModeEnabled: false,
+    'general_setting.invitation_enabled': true,
+  };
+  useEffect(() => {
+    const currentInputs = { ...DEFAULT_INPUTS };
model/user.go (1)

931-999: Group rename: add no-op guard and surface empty updates.

Minor: avoid work when names equal; optionally error when RowsAffected==0 so callers can detect no changes.

 func UpdateUsersGroupName(oldGroupName, newGroupName string) error {
   if oldGroupName == "" || newGroupName == "" {
     return errors.New("分组名称不能为空")
   }
+  if oldGroupName == newGroupName {
+    common.SysLog("分组名称未变化,跳过更新")
+    return nil
+  }
@@
-  // 提交事务
+  // 提交事务
   if err := tx.Commit().Error; err != nil {
     common.SysLog(fmt.Sprintf("提交事务失败: %s", err.Error()))
     return err
   }
+  if result.RowsAffected == 0 {
+    return fmt.Errorf("未找到使用分组 '%s' 的用户", oldGroupName)
+  }
controller/user_group.go (2)

236-240: Use the shared helper for reserved groups (case-insensitive).

Avoid drift by reusing isReservedGroup.

- if group.Name == "default" || group.Name == "vip" || group.Name == "svip" {
+ if isReservedGroup(group.Name) {

318-338: Topup ratio writer must be mutex-protected to avoid concurrent map writes.

GetTopupGroupRatioCopy is safe, but common.UpdateTopupGroupRatioByJSONString currently writes without locking.

Add a write lock in common/topup-ratio.go:

 func UpdateTopupGroupRatioByJSONString(jsonStr string) error {
-  TopupGroupRatio = make(map[string]float64)
-  return json.Unmarshal([]byte(jsonStr), &TopupGroupRatio)
+  topupGroupRatioMutex.Lock()
+  defer topupGroupRatioMutex.Unlock()
+  TopupGroupRatio = make(map[string]float64)
+  return json.Unmarshal([]byte(jsonStr), &TopupGroupRatio)
}
controller/user.go (4)

395-401: Validate quota input before processing.

Reject non-positive quotas early to avoid unnecessary calls.

 if err := c.ShouldBindJSON(&tran); err != nil {
   common.ApiError(c, err)
   return
 }
+if tran.Quota <= 0 {
+  c.JSON(http.StatusOK, gin.H{"success": false, "message": "划转额度必须大于0"})
+  return
+}
 err = user.TransferAffQuotaToQuota(tran.Quota)

858-879: Avoid double-encoding system config in cleaned setting.

Store sidebar_system_config as an object, not a JSON string.

- systemConfigJSON, err := json.Marshal(systemSidebarConfig)
- if err == nil {
-   extendedSetting := map[string]interface{}{
-     "sidebar_modules":        userSetting.SidebarModules,
-     "sidebar_system_config": string(systemConfigJSON),
-   }
+ {
+   extendedSetting := map[string]interface{}{
+     "sidebar_modules":         userSetting.SidebarModules,
+     "sidebar_system_config":   systemSidebarConfig,
+   }

805-807: Default section.enabled to a boolean (avoid null in JSON).

When user preference is absent, enabled becomes null; default it to true.

- filteredSection := map[string]interface{}{
-   "enabled": userSection["enabled"],
- }
+ enabled := true
+ if v, ok := userSection["enabled"].(bool); ok { enabled = v }
+ filteredSection := map[string]interface{}{"enabled": enabled}

Apply in both helper functions.

Also applies to: 911-914


555-679: Parameter userSetting is unused in calculateFinalSidebarConfig.

Remove it to reduce confusion, and update call sites.

- func calculateFinalSidebarConfig(userRole int, userSetting dto.UserSetting) map[string]interface{} {
+ func calculateFinalSidebarConfig(userRole int) map[string]interface{} {
- systemSidebarConfig := calculateFinalSidebarConfig(userRole, userSetting)
+ systemSidebarConfig := calculateFinalSidebarConfig(userRole)

Also applies to: 466-468

model/user_group.go (1)

102-137: Optional: seed defaults in a transaction.

Ensure all-or-nothing creation of default groups.

tx := DB.Begin()
for _, g := range defaultGroups {
  if err := tx.Create(g).Error; err != nil { tx.Rollback(); return err }
}
return tx.Commit().Error
web/src/hooks/model-pricing/useModelPricingData.jsx (3)

178-193: Combine the two sorts into a single stable comparator

Two sequential sorts are redundant and the latter overrides the former. Use one comparator to enforce GPT-priority, then quota_type, then name.

-    models.sort((a, b) => {
-      return a.quota_type - b.quota_type;
-    });
-
-    models.sort((a, b) => {
-      if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
-        return -1;
-      } else if (
-        !a.model_name.startsWith('gpt') &&
-        b.model_name.startsWith('gpt')
-      ) {
-        return 1;
-      } else {
-        return a.model_name.localeCompare(b.model_name);
-      }
-    });
+    models.sort((a, b) => {
+      const aG = a.model_name.startsWith('gpt');
+      const bG = b.model_name.startsWith('gpt');
+      if (aG !== bG) return aG ? -1 : 1;
+      if (a.quota_type !== b.quota_type) return a.quota_type - b.quota_type;
+      return a.model_name.localeCompare(b.model_name);
+    });

200-245: Finalize loading state and avoid back-nav loops on redirects

Set loading in finally and use replace navigation on 403 to prevent stuck spinners and history back loops.

-  const loadPricing = async () => {
+  const loadPricing = async () => {
     setLoading(true);
-    try {
+    try {
       let url = '/api/pricing';
       const res = await API.get(url);
@@
-      if (error.response && error.response.status === 403) {
+      if (error.response && error.response.status === 403) {
         // 未登录用户跳转登录页;已登录但无权限(理论上极少见)跳转禁止访问
         if (!userState?.user) {
-          navigate('/login');
+          navigate('/login', { replace: true });
         } else {
-          navigate('/forbidden');
+          navigate('/forbidden', { replace: true });
         }
-        return;
+        return;
       }
       // 其他错误正常处理
       showError(error.message || t('加载模型广场数据失败'));
-    }
-    setLoading(false);
+    } finally {
+      setLoading(false);
+    }
   };

218-225: Minor: vendorMap build can use reduce for clarity

Not required, but reduces mutable state and loops.

-        const vendorMap = {};
-        if (Array.isArray(vendors)) {
-          vendors.forEach((v) => {
-            vendorMap[v.id] = v;
-          });
-        }
+        const vendorMap = Array.isArray(vendors)
+          ? vendors.reduce((acc, v) => ((acc[v.id] = v), acc), {})
+          : {};
middleware/auth.go (1)

226-238: Signature includes userId but unused

Either use userId for future per-user gating or remove it from hasModulePermission signature and call site to reduce noise.

-func hasModulePermission(userRole int, userId int, modulePath string) bool {
+func hasModulePermission(userRole int, modulePath string) bool {
@@
-    if !hasModulePermission(userRole, userId, modulePath) {
+    if !hasModulePermission(userRole, modulePath) {
controller/pricing.go (1)

95-111: Avoid DB hit when only presence is needed

If upstream middlewares already set role on context, prefer c.GetInt("role") >= RoleCommonUser to skip GetUserById on hot path.

-		user, err := model.GetUserById(userId.(int), false)
-		if err != nil {
-			return false, http.StatusInternalServerError, "用户信息获取失败"
-		}
-
-		if user.Role >= common.RoleCommonUser {
+		if c.GetInt("role") >= common.RoleCommonUser {
 			return true, 0, ""
 		}
-		return false, http.StatusForbidden, "权限不足"
+		// 兜底再查一次防止缺失
+		user, err := model.GetUserById(userId.(int), false)
+		if err != nil {
+			return false, http.StatusInternalServerError, "用户信息获取失败"
+		}
+		if user.Role >= common.RoleCommonUser {
+			return true, 0, ""
+		}
+		return false, http.StatusForbidden, "权限不足"
web/src/constants/user.constants.js (4)

44-49: Freeze role enum to prevent accidental mutation.

Make the roles immutable and slightly safer to import.

-export const USER_ROLES = {
+export const USER_ROLES = Object.freeze({
   GUEST: 0,      // RoleGuestUser
   COMMON: 1,     // RoleCommonUser
   ADMIN: 10,     // RoleAdminUser
   ROOT: 100,     // RoleRootUser
-};
+});

74-76: Add isGuest for completeness.

Completes the predicate set and simplifies guest gating.

 export const isCommonUser = (role) => {
   return role === USER_ROLES.COMMON;
 };
+
+export const isGuest = (role) => {
+  return role === USER_ROLES.GUEST;
+};

84-97: Prefer i18n keys over literal strings.

Using keys keeps translations maintainable and extractable; provide fallbacks if your i18n lib supports it.

 export const getRoleDisplayName = (role, t) => {
   switch (role) {
     case USER_ROLES.COMMON:
-      return t('普通用户');
+      return t('role.common', '普通用户');
     case USER_ROLES.ADMIN:
-      return t('管理员');
+      return t('role.admin', '管理员');
     case USER_ROLES.ROOT:
-      return t('超级管理员');
+      return t('role.root', '超级管理员');
     case USER_ROLES.GUEST:
-      return t('访客');
+      return t('role.guest', '访客');
     default:
-      return t('未知身份');
+      return t('role.unknown', '未知身份');
   }
 };

56-58: Centralize role-check helpers in modals and actions
Replace local isRoot/isAdmin definitions with the shared helpers from web/src/constants/user.constants.js in:

  • web/src/components/table/users/modals/UserGroupManagement.jsx (lines 63–64)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (lines 51–52)
  • web/src/components/table/users/UsersActions.jsx (lines 35–36)
web/src/components/auth/ModuleRoute.jsx (4)

6-7: Drop unused USER_ROLES import after refactor.

-import { USER_ROLES } from '../../constants/user.constants';
+// 权限判断依赖服务端 finalConfig,不再直接使用本地角色常量

81-84: Validate “console.detail always allowed” policy.

Confirm this is intended even when the user is not authenticated; otherwise gate it via config too.


50-51: Effect churn risk due to object identity of finalConfig.

Depending on an object causes frequent re-runs. If useSidebar can expose a stable version/timestamp, depend on that instead.


1-151: Defense-in-depth: make the client safe on server fetch failures.

useSidebar falls back to a permissive default that includes admin modules; this can expose admin pages client-side if /api/user/self fails. Default closed.

Suggested change in web/src/hooks/common/useSidebar.js:

-  const defaultSidebarConfig = {
-    chat: { enabled: true, playground: true, chat: true },
-    console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
-    personal: { enabled: true, topup: true, personal: true },
-    admin: {
-      enabled: true,
-      channel: true,
-      models: true,
-      redemption: true,
-      user: { enabled: true, groupManagement: true },
-      setting: true
-    }
-  };
+  // 安全缺省:服务端不可用时全部关闭
+  const defaultSidebarConfig = {
+    chat: { enabled: false },
+    console: { enabled: false },
+    personal: { enabled: false },
+    admin: { enabled: false },
+  };
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (3)

340-345: Stabilize useEffect deps to avoid needless re-renders.

Avoid putting a function in deps; depend on a boolean gate instead.

Apply this diff:

-  // 只有权限加载完成且有边栏设置权限时才加载配置
-  if (!permissionsLoading && hasSidebarSettingsPermission()) {
-    loadConfigs();
-  }
-}, [permissionsLoading, hasSidebarSettingsPermission]);
+  // 只有权限加载完成且有边栏设置权限时才加载配置
+  const canLoadConfigs = !permissionsLoading && hasSidebarSettingsPermission();
+  if (!canLoadConfigs) return;
+  loadConfigs();
+}, [canLoadConfigs]);

283-297: Parse setting once and reuse.

JSON.parse(userRes.data.data.setting) is done twice. Parse once, reuse for both system and user configs to reduce error surface and work.

Apply this sketch:

-        if (userRes.data.success && userRes.data.data.setting) {
-          // 从setting字段中获取系统权限信息
-          try {
-            const setting = JSON.parse(userRes.data.data.setting);
+        if (userRes.data.success && userRes.data.data.setting) {
+          let parsedSetting;
+          try {
+            parsedSetting = JSON.parse(userRes.data.data.setting);
             if (setting.sidebar_system_config) {
-              const systemConfig = JSON.parse(setting.sidebar_system_config);
+              const systemConfig = JSON.parse(parsedSetting.sidebar_system_config);
               setAdminConfig(systemConfig);
             } else {
               // 如果没有系统配置,使用默认配置
               setAdminConfig(getDefaultSystemConfig());
               console.log('使用默认系统配置');
             }
           } catch (error) {
             console.error('解析系统配置失败:', error);
             setAdminConfig(getDefaultSystemConfig());
           }
-          // 从同一个setting字段中获取用户的原始偏好设置
-          try {
-            const setting = JSON.parse(userRes.data.data.setting);
-            const systemConfig = setting.sidebar_system_config ? JSON.parse(setting.sidebar_system_config) : getDefaultSystemConfig();
+          try {
+            const systemConfig = parsedSetting?.sidebar_system_config ? JSON.parse(parsedSetting.sidebar_system_config) : getDefaultSystemConfig();
-            if (setting.sidebar_modules) {
+            if (parsedSetting?.sidebar_modules) {
-              if (typeof setting.sidebar_modules === 'string') {
-                userConf = JSON.parse(setting.sidebar_modules);
+              if (typeof parsedSetting.sidebar_modules === 'string') {
+                userConf = JSON.parse(parsedSetting.sidebar_modules);
               } else {
-                userConf = setting.sidebar_modules;
+                userConf = parsedSetting.sidebar_modules;
               }
             } else {
               const defaultConfig = generateDefaultConfig(systemConfig);
               setSidebarModulesUser(defaultConfig);
             }
           } catch (error) {
             console.error('解析用户设置失败:', error);
-            const defaultConfig = generateDefaultConfig();
+            const defaultConfig = generateDefaultConfig();
             setSidebarModulesUser(defaultConfig);
           }

Also applies to: 299-317


212-213: Tone down console noise for production.

Gate verbose logs with process.env.NODE_ENV !== 'production' or remove them to keep console clean.

Example:

- console.log('用户边栏配置保存成功');
+ if (process.env.NODE_ENV !== 'production') console.log('用户边栏配置保存成功');

Also applies to: 228-235, 243-244, 250-262, 331-337, 409-417, 421-423

📜 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 ed5d9ea and 7765154.

📒 Files selected for processing (18)
  • common/topup-ratio.go (2 hunks)
  • controller/misc.go (4 hunks)
  • controller/pricing.go (3 hunks)
  • controller/user.go (10 hunks)
  • controller/user_group.go (1 hunks)
  • middleware/auth.go (2 hunks)
  • model/user.go (7 hunks)
  • model/user_group.go (1 hunks)
  • web/index.html (1 hunks)
  • web/src/components/auth/ModuleRoute.jsx (1 hunks)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
  • web/src/constants/user.constants.js (1 hunks)
  • web/src/helpers/data.js (1 hunks)
  • web/src/hooks/model-pricing/useModelPricingData.jsx (2 hunks)
  • web/src/i18n/locales/en.json (3 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (3 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx
  • controller/misc.go
  • web/index.html
🧰 Additional context used
🧬 Code graph analysis (12)
web/src/constants/user.constants.js (2)
web/src/components/table/users/modals/EditUserGroupModal.jsx (2)
  • isAdmin (52-52)
  • isRoot (51-51)
web/src/components/table/users/modals/UserGroupManagement.jsx (2)
  • isAdmin (64-64)
  • isRoot (63-63)
controller/pricing.go (2)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleCommonUser (134-134)
model/user.go (1)
  • GetUserById (294-306)
model/user.go (6)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/constants.go (2)
  • QuotaForInvitee (100-100)
  • QuotaForInviter (99-99)
model/log.go (1)
  • RecordLog (78-94)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
common/redis.go (1)
  • RedisEnabled (17-17)
web/src/components/auth/ModuleRoute.jsx (3)
web/src/hooks/common/useSidebar.js (4)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
  • sidebarConfig (31-31)
web/src/constants/user.constants.js (2)
  • USER_ROLES (44-49)
  • USER_ROLES (44-49)
web/src/components/common/ui/Loading.jsx (1)
  • Loading (23-29)
middleware/auth.go (2)
common/constants.go (4)
  • RoleRootUser (136-136)
  • RoleAdminUser (135-135)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
common/sys_log.go (1)
  • SysLog (10-13)
web/src/hooks/model-pricing/useModelPricingData.jsx (2)
web/src/hooks/models/useModelsData.jsx (1)
  • vendorMap (101-107)
web/src/helpers/utils.jsx (1)
  • showError (118-147)
web/src/components/table/users/modals/UserGroupManagement.jsx (3)
web/src/components/table/users/modals/EditUserGroupModal.jsx (7)
  • loading (42-42)
  • sidebarLoading (43-43)
  • getUserRole (46-49)
  • isRoot (51-51)
  • isAdmin (52-52)
  • hasGroupManagementPermission (55-82)
  • EditUserGroupModal (39-274)
web/src/hooks/common/useSidebar.js (4)
  • loading (32-32)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
web/src/helpers/utils.jsx (2)
  • showError (118-147)
  • showSuccess (153-155)
controller/user_group.go (8)
model/user_group.go (5)
  • GetAllUserGroups (51-55)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (78-86)
  • GetUserGroupById (58-65)
  • IsUserGroupInUse (89-93)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-49)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
common/topup-ratio.go (2)
  • GetTopupGroupRatioCopy (16-24)
  • UpdateTopupGroupRatioByJSONString (34-37)
model/user_group.go (3)
common/utils.go (1)
  • GetTimestamp (192-194)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-49)
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)
web/src/helpers/render.jsx (1)
  • key (426-426)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)
web/src/components/settings/personal/cards/NotificationSettings.jsx (5)
  • sidebarModulesUser (60-87)
  • adminConfig (88-88)
  • permissionsLoading (91-97)
  • getDefaultSystemConfig (174-203)
  • handleModuleChange (116-127)
web/src/hooks/common/useUserPermissions.js (1)
  • hasSidebarSettingsPermission (58-60)
controller/user.go (5)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
dto/user_settings.go (1)
  • UserSetting (3-13)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleAdminUser (135-135)
model/user.go (1)
  • GenerateDefaultSidebarConfigForRole (96-161)
common/sys_log.go (1)
  • SysLog (10-13)
🪛 Biome (2.1.2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

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

Hooks should not be called after an early return.

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)

🔇 Additional comments (14)
web/src/helpers/data.js (1)

24-33: Resolved undefined/empty system name persistence; event always emitted — LGTM

This addresses the prior concern, avoids persisting "undefined"/blank values, and guarantees a reset signal. Looks good.

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

2083-2130: Remove duplicate translation keys in en.json
In web/src/i18n/locales/en.json, delete the redundant entries at the end of the file to prevent last-wins overrides of earlier definitions:

@@ web/src/i18n/locales/en.json:2083-2130
-  "分组名称": "Group Name",   # duplicate of line 633
-  "创建时间": "Created Time",  # duplicate of line 307
-  "默认": "default",          # duplicate of line 516
model/user.go (3)

96-161: Sidebar defaults by role: LGTM.

Exported GenerateDefaultSidebarConfigForRole and nested admin.user.groupManagement are sensible defaults.


410-417: Post-create sidebar init: LGTM.

Using GenerateDefaultSidebarConfigForRole for the created user is appropriate.


424-437: Invitation gating is correct and fixes prior count issue.

Gate rewards by InvitationEnabled and always call inviteUser to count referrals even when rewards are 0.

controller/user.go (1)

379-388: Invitation feature flag gating is correct.

Early-return with a clear message when disabled.

Also applies to: 415-424

middleware/auth.go (1)

182-194: Context-first auth fallback looks good

ModuleAuth now prefers gin.Context values and safely falls back to session; avoids panics from unsafe assertions.

controller/pricing.go (1)

4-6: Confirm internal OptionMap key vs JSON property naming: the code uses the internal key "HeaderNavModules" (e.g. in controller/pricing.go:68) but exposes it as "header_nav_modules" in JSON (e.g. controller/misc.go:102). Verify that the settings UI writes to the internal key "HeaderNavModules" and that both the UI and API consumers expect the JSON field "header_nav_modules".

web/src/constants/user.constants.js (1)

44-49: USER_ROLES constants align with backend: Values in web/src/constants/user.constants.js match those in common/constants.go; no changes required.

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

139-151: LGTM: clean loading and cancellation handling.

Loading state and unmount guard are correct.

web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (4)

62-71: Early-loading guard reads cleanly.

The loading card now renders only after all hooks are declared. No hook-order risk.


468-502: UI grid rendering looks solid.

Good accessibility knobs (disabled when section off) and stable keys per module; switch binding is correct.


79-111: Confirm admin.user shape change is propagated
Ran repo-wide searches for any code treating admin.user as a boolean and found no matches. Manually verify all consumers (menu guards, serializers, migrations, etc.) handle the new { enabled, groupManagement } object shape.


251-254: Backend correctly handles sidebar_modules.
The UpdateSelf handler detects the sidebar_modules field, merges it into the user’s setting JSON via user.SetSetting(...), and saves it with user.Update(). No silent drops—no change required.

Comment thread common/topup-ratio.go
Comment thread controller/pricing.go Outdated
Comment thread controller/pricing.go
Comment thread controller/user_group.go
Comment thread controller/user_group.go
Comment thread web/src/components/table/users/modals/UserGroupManagement.jsx
Comment thread web/src/components/table/users/modals/UserGroupManagement.jsx
Comment thread web/src/helpers/data.js
Comment thread web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
Comment thread web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

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

Caution

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

⚠️ Outside diff range comments (3)
web/src/components/auth/PasswordResetForm.jsx (2)

126-146: Prevent form auto-submit/double-submit: use onSubmit and call preventDefault.

The Button has htmlType='submit' and an onClick handler, but the Form lacks onSubmit and handleSubmit doesn’t prevent default. This can cause a page refresh or double invocation.

Apply:

-                <Form className='space-y-3'>
+                <Form className='space-y-3' onSubmit={handleSubmit}>
...
-                    <Button
+                    <Button
                      theme='solid'
                      className='w-full !rounded-full'
                      type='primary'
                      htmlType='submit'
-                      onClick={handleSubmit}
                       loading={loading}
                       disabled={disableButton}
                     >

And in handleSubmit:

 async function handleSubmit(e) {
+  e?.preventDefault?.();
   if (!email) {

94-96: Avoid leaking PII/token in URL: encode email or use POST.

Email and Turnstile token in a GET query can be logged by proxies/browsers. At minimum URL-encode; ideally switch to POST with a JSON body.

Quick mitigation:

-    const res = await API.get(
-      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
-    );
+    const res = await API.get(
+      `/api/reset_password?email=${encodeURIComponent(email)}&turnstile=${encodeURIComponent(turnstileToken)}`,
+    );

Preferred:

-    const res = await API.get(
-      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
-    );
+    const res = await API.post('/api/reset_password', {
+      email,
+      turnstile: turnstileToken,
+    });
web/src/hooks/common/useSidebar.js (1)

35-65: Use a safe default: keep admin disabled to avoid exposing admin UI on failures.

   const defaultSidebarConfig = {
@@
-    admin: {
-      enabled: true,
-      channel: true,
-      models: true,
-      redemption: true,
-      user: {
-        enabled: true,
-        groupManagement: true  // 默认启用分组管理
-      },
-      setting: true
-    }
+    admin: {
+      enabled: false,
+      channel: false,
+      models: false,
+      redemption: false,
+      user: {
+        enabled: false,
+        groupManagement: false
+      },
+      setting: false
+    }
   };
♻️ Duplicate comments (10)
web/src/components/topup/index.jsx (1)

265-301: Security concern: Default to disabled on API failure.

Currently, when the API call fails or returns non-success, the code defaults to enabling the invitation feature (setInvitationEnabled(true)). This could expose features that should be gated. Based on the past review comments, this was already flagged but appears unresolved.

For security and consistency, default to disabled state on errors:

       } else {
-        // API调用失败,使用后端默认值(true)
-        //console.log('status接口调用失败,使用默认值true');
-        setInvitationEnabled(true);
-        setInvitationConfigLoaded(true);
-        if (!affFetchedRef.current) {
-          affFetchedRef.current = true;
-          getAffLink();
-        }
+        // API调用失败,安全起见默认关闭
+        setInvitationEnabled(false);
+        setInvitationConfigLoaded(true);
       }
     } catch (error) {
-      //console.error('获取邀请功能配置失败:', error);
-      // 出错时使用后端默认值(true)
-      setInvitationEnabled(true);
-      setInvitationConfigLoaded(true);
-      if (!affFetchedRef.current) {
-        affFetchedRef.current = true;
-        getAffLink();
-      }
+      // 出错时安全起见默认关闭
+      setInvitationEnabled(false);
+      setInvitationConfigLoaded(true);
     }
model/main.go (1)

267-271: MySQL will fail on UserGroup partial unique index; switch to composite (name, deleted_at).

AutoMigrate will error on MySQL because UserGroup currently uses a partial unique index (where:deleted_at IS NULL) in model/user_group.go. Replace it with a composite unique index shared by Name and DeletedAt.

Apply in model/user_group.go:

 type UserGroup struct {
-    Name        string         `json:"name" gorm:"size:64;not null;uniqueIndex:uk_user_group_name,where:deleted_at IS NULL"`
+    Name        string         `json:"name" gorm:"size:64;not null;uniqueIndex:uk_user_group_name_del"`
     Description string         `json:"description,omitempty" gorm:"type:varchar(255)"`
     Ratio       float64        `json:"ratio" gorm:"type:decimal(10,4);default:1.0"`
     CreatedTime int64          `json:"created_time" gorm:"bigint"`
     UpdatedTime int64          `json:"updated_time" gorm:"bigint"`
-    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
+    DeletedAt   gorm.DeletedAt `json:"-" gorm:"uniqueIndex:uk_user_group_name_del"`
 }
web/src/hooks/common/useSidebar.js (4)

30-30: Create a stable source id to suppress self-handled refreshes.

 export const useSidebar = () => {
+  const instanceId = useRef(Symbol('useSidebarInstance'));

86-91: Avoid double-loading: include sourceId in the event and let listeners ignore self.

   const refreshUserConfig = async () => {
     await loadSidebarConfig();
-    // 触发全局刷新事件,通知所有useSidebar实例更新
-    sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
+    // 触发全局刷新事件,通知所有useSidebar实例更新(携带来源)
+    sidebarEventTarget.dispatchEvent(
+      new CustomEvent(SIDEBAR_REFRESH_EVENT, { detail: { sourceId: instanceId.current } })
+    );
   };

98-109: Ignore self-originated refresh events to prevent redundant reloads.

-  useEffect(() => {
-    const handleRefresh = () => {
-      loadSidebarConfig();
-    };
-    sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
-    return () => {
-      sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
-    };
-  }, []);
+  useEffect(() => {
+    const handleRefresh = (e) => {
+      if (e?.detail?.sourceId === instanceId.current) return;
+      loadSidebarConfig();
+    };
+    sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
+    return () => {
+      sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
+    };
+  }, []);

20-20: Import useRef for stable instance id.

-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
web/src/components/table/users/modals/UserGroupManagement.jsx (2)

57-65: Deduplicate role/permission logic via a shared hook.

This logic duplicates EditUserGroupModal and will drift. Extract to a common hook (useGroupManagement) and consume here.

I can generate the hook and integrate it if you want.


17-18: Fix stray "+*/" in license header (build-breaking).

There's a literal "+" before the comment terminator. Remove it to restore valid JS.

-For commercial licensing, please contact support@quantumnous.com
-+*/
+For commercial licensing, please contact support@quantumnous.com
+*/
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)

57-61: Fix Hooks order: early returns before useEffect violate Rules of Hooks.

useEffect must be declared before any conditional returns. Move the two early returns below the useEffect. Static analysis still flags this.

-  // 如果没有边栏设置权限,不显示此组件
-  if (!permissionsLoading && !hasSidebarSettingsPermission()) {
-    return null;
-  }
-
-  // 如果配置还在加载中,显示加载状态
-  if (configLoading || permissionsLoading) {
-    return (
-      <Card className='!rounded-2xl shadow-sm border-0'>
-        <div className='flex items-center justify-center py-8'>
-          <div className='text-gray-500'>{t('加载中...')}</div>
-        </div>
-      </Card>
-    );
-  }
+  // (moved below useEffect)
   useEffect(() => {
     const loadConfigs = async () => {
       try {
         setConfigLoading(true);
         // ...
       } finally {
         setConfigLoading(false);
       }
     };
     if (!permissionsLoading && hasSidebarSettingsPermission()) {
       loadConfigs();
     }
-  }, [permissionsLoading, hasSidebarSettingsPermission]);
+  }, [permissionsLoading, hasSidebarSettingsPermission]);
+
+  // 如果没有边栏设置权限,不显示此组件
+  if (!permissionsLoading && !hasSidebarSettingsPermission()) {
+    return null;
+  }
+  // 如果配置还在加载中,显示加载状态
+  if (configLoading || permissionsLoading) {
+    return (
+      <Card className='!rounded-2xl shadow-sm border-0'>
+        <div className='flex items-center justify-center py-8'>
+          <div className='text-gray-500'>{t('加载中...')}</div>
+        </div>
+      </Card>
+    );
+  }

Also applies to: 62-71, 274-345

controller/user.go (1)

555-679: Refactor deeply nested sidebar computation for maintainability.
The loops/type assertions are still quite nested. Extract focused helpers (section/module/nested-module processors) to reduce cyclomatic complexity and ease testing. This mirrors the earlier suggestion and remains non-blocking.

Also consider caching the parsed SidebarModulesAdmin (with a version/hash) to avoid JSON unmarshal on every GetSelf, and invalidate on OptionMap change.

🧹 Nitpick comments (21)
web/src/components/auth/PasswordResetForm.jsx (1)

108-113: Remove duplicate max-width wrappers to avoid over-constraining layout.

You set max-w-md on both the outer and inner containers. Keep one to prevent unexpected wrapping at certain breakpoints.

Apply:

-          <div className='w-full max-w-md'>
+          <div className='w-full'>
web/src/hooks/model-pricing/useModelPricingData.jsx (3)

200-244: Consider using StatusContext first before making API call.

The current implementation always makes a fresh API call to /api/status. Since the pricing module configuration might already be available in StatusContext, consider checking the context first to reduce redundant API calls.

 const loadPricing = async () => {
   setLoading(true);
+  
+  // Check if pricing is disabled in status context first
+  const pricingEnabled = statusState?.status?.pricing_enabled;
+  if (pricingEnabled === false) {
+    // Module is disabled, redirect appropriately
+    if (!userState?.user) {
+      navigate('/login');
+    } else {
+      navigate('/forbidden');
+    }
+    setLoading(false);
+    return;
+  }
+  
   try {
     let url = '/api/pricing';
     const res = await API.get(url);

243-243: Consider using a more specific error message key.

The error message uses a generic translation key. Consider having different messages for different error scenarios (network error, server error, etc.).

-      showError(error.message || t('加载模型广场数据失败'));
+      const errorMessage = error.response?.status >= 500 
+        ? t('服务器错误,请稍后再试')
+        : error.message || t('加载模型广场数据失败');
+      showError(errorMessage);

217-227: Ensure vendorMap is always an object.

While the code defensively checks if vendors is an array, it should also ensure vendorMap is always initialized as an object even when vendors is undefined/null.

The current implementation already handles this well by initializing vendorMap = {} before the condition. The defensive programming here is good.

web/src/components/topup/index.jsx (1)

268-280: Consider leveraging StatusContext to avoid redundant API calls.

Since the invitation configuration is already available in StatusContext (as seen in the summary), consider checking the context first before making a separate API call.

 const getInvitationConfig = async () => {
+  // First check if configuration is already available in StatusContext
+  const contextValue = statusState?.status?.invitation_enabled;
+  if (contextValue !== undefined) {
+    const enabled = contextValue === true;
+    setInvitationEnabled(enabled);
+    setInvitationConfigLoaded(true);
+    if (enabled && !affFetchedRef.current) {
+      affFetchedRef.current = true;
+      getAffLink();
+    }
+    return;
+  }
+  
+  // Fallback to API call if not in context
   try {
     const res = await API.get('/api/status');
web/src/App.jsx (1)

226-230: Consider extracting Suspense wrapper for cleaner code.

Multiple routes have the same pattern of ModuleRoute wrapping Suspense wrapping the component. Consider creating a wrapper component to reduce repetition.

// Create a helper component
const SuspendedModuleRoute = ({ modulePath, children }) => (
  <ModuleRoute modulePath={modulePath}>
    <Suspense fallback={<Loading />} key={location.pathname}>
      {children}
    </Suspense>
  </ModuleRoute>
);

// Then use it in routes:
<Route
  path='/console/setting'
  element={
    <AdminRoute>
      <SuspendedModuleRoute modulePath="admin.setting">
        <Setting />
      </SuspendedModuleRoute>
    </AdminRoute>
  }
/>

Also applies to: 238-242, 250-254, 262-264, 272-276, 284-288, 296-300, 334-338, 346-350

controller/misc.go (2)

317-367: Anonymous no longer receives raw config; disabled modules are removed.

Solid fix aligning with prior guidance. Optional: always return a JSON string type (e.g., return "{}" instead of raw on non-string input) to keep client handling uniform.

- if !ok || headerNavModulesStr == "" {
-   return headerNavModulesRaw
- }
+ if !ok || headerNavModulesStr == "" {
+   return "{}"
+ }

509-528: Pricing special-case honors requireAuth; baseline allowlist for basic links is fine.

If product direction changes, consider centralizing module permission policy to avoid drift between header and sidebar.

web/src/components/table/users/modals/UserGroupManagement.jsx (2)

195-209: Avoid hard-coding CN source descriptions for i18n.

Mapping based on exact CN strings is brittle. Prefer a server flag (e.g., system: true) or name-only mapping.

-  const getGroupDescription = (groupName, originalDescription) => {
-    // 对于系统默认分组,使用翻译
-    if (groupName === 'default' && originalDescription === '默认分组') {
-      return t('默认分组');
-    }
-    if (groupName === 'vip' && originalDescription === 'VIP分组') {
-      return t('VIP分组');
-    }
-    if (groupName === 'svip' && originalDescription === 'SVIP分组') {
-      return t('SVIP分组');
-    }
-    // 对于用户自定义分组,使用原始描述
-    return originalDescription;
-  };
+  const getGroupDescription = (groupName, originalDescription) => {
+    if (groupName === 'default') return t('默认分组');
+    if (groupName === 'vip') return t('VIP分组');
+    if (groupName === 'svip') return t('SVIP分组');
+    return originalDescription;
+  };

248-256: Format ratio consistently and guard non-numeric input.

Display as fixed decimals to improve readability and avoid “NaN”.

-      render: (text) => (
+      render: (text) => (
         <Tag color='cyan' shape='circle'>
-          {text}
+          {Number.isFinite(Number(text)) ? Number(text).toFixed(2) : '—'}
         </Tag>
       ),
common/topup-ratio.go (1)

36-45: Optional: use common.Unmarshal for consistency.

If the codebase standardizes JSON decode via common.Unmarshal, switch to it here.

- var tmp map[string]float64
- if err := json.Unmarshal([]byte(jsonStr), &tmp); err != nil {
+ var tmp map[string]float64
+ if err := Unmarshal([]byte(jsonStr), &tmp); err != nil {
   return err
 }
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (2)

115-150: Avoid duplicating default config in multiple places.

Define a single defaultAdminModules constant and reuse in state init, reset, and error fallback.

-  const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({
-    chat: { enabled: true, playground: true, chat: true },
-    console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
-    personal: { enabled: true, topup: true, personal: true },
-    admin: {
-      enabled: true,
-      channel: true,
-      models: true,
-      redemption: true,
-      user: { enabled: true, groupManagement: true },
-      setting: true
-    }
-  });
+  const defaultAdminModules = {
+    chat: { enabled: true, playground: true, chat: true },
+    console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
+    personal: { enabled: true, topup: true, personal: true },
+    admin: { enabled: true, channel: true, models: true, redemption: true, user: { enabled: true, groupManagement: true }, setting: true }
+  };
+  const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState(defaultAdminModules);
...
-    const defaultModules = {
-      chat: { enabled: true, playground: true, chat: true },
-      console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
-      personal: { enabled: true, topup: true, personal: true },
-      admin: { enabled: true, channel: true, models: true, redemption: true, user: { enabled: true, groupManagement: true }, setting: true }
-    };
-    setSidebarModulesAdmin(defaultModules);
+    setSidebarModulesAdmin(defaultAdminModules);
...
-        const defaultModules = { ...same as above... };
-        setSidebarModulesAdmin(defaultModules);
+        setSidebarModulesAdmin(defaultAdminModules);

415-467: Simplify redundant conditionals in the user sub-switch block.

The inner ternary repeats module.key === 'user'; simplify for readability.

- {module.key === 'user' && (
-   module.key === 'user'
-     ? sidebarModulesAdmin[section.key]?.user?.enabled
-     : sidebarModulesAdmin[section.key]?.[module.key]
- ) && (
+ {module.key === 'user' && sidebarModulesAdmin[section.key]?.user?.enabled && (
    <div style={{ borderTop: '1px solid var(--semi-color-border)', marginTop: '12px', paddingTop: '12px' }}>
      ...
      <Switch
-       checked={sidebarModulesAdmin[section.key]?.user?.groupManagement || false}
+       checked={!!sidebarModulesAdmin[section.key]?.user?.groupManagement}
        onChange={handleUserGroupManagementChange}
        size="small"
-       disabled={!sidebarModulesAdmin[section.key]?.enabled || !(
-         module.key === 'user'
-           ? sidebarModulesAdmin[section.key]?.user?.enabled
-           : sidebarModulesAdmin[section.key]?.[module.key]
-       )}
+       disabled={!sidebarModulesAdmin[section.key]?.enabled}
      />
      ...
    </div>
 )}
controller/user_group.go (2)

50-53: Remove redundant strings.ToLower around isReservedGroup.

isReservedGroup already normalizes case internally.

- if isReservedGroup(strings.ToLower(g.Name)) {
+ if isReservedGroup(g.Name) {
- if isReservedGroup(strings.ToLower(g.Name)) {
+ if isReservedGroup(g.Name) {
- if isReservedGroup(strings.ToLower(oldGroup.Name)) {
+ if isReservedGroup(oldGroup.Name) {

Also applies to: 155-159, 163-167


271-294: Prevent lost updates when syncing JSON-backed settings (read-modify-write race).

Current pattern reads a copy, mutates, then overwrites DB/memory; concurrent writers can clobber changes. Add atomic “mutate-under-lock + persist” helpers for each setting and call them here.

Example for ratio_setting:

- groupRatio := ratio_setting.GetGroupRatioCopy()
- if add { groupRatio[groupName] = ratio } else { delete(groupRatio, groupName) }
- jsonBytes, err := json.Marshal(groupRatio)
- ...
- if err := model.UpdateOption("GroupRatio", string(jsonBytes)); err != nil { ... }
- return ratio_setting.UpdateGroupRatioByJSONString(string(jsonBytes))
+ return ratio_setting.MutateAndPersistGroupRatio(func(m map[string]float64) {
+   if add { m[groupName] = ratio } else { delete(m, groupName) }
+ })

Helper to add (outside this file):

// ratio_setting/store.go
var groupRatioMutex sync.RWMutex
func MutateAndPersistGroupRatio(mut func(map[string]float64)) error {
  groupRatioMutex.Lock()
  defer groupRatioMutex.Unlock()
  // mutate in-memory map
  mut(groupRatio)
  // persist snapshot
  b, err := json.Marshal(groupRatio)
  if err != nil { return err }
  if err := model.UpdateOption("GroupRatio", string(b)); err != nil { return err }
  return nil
}

Mirror the same pattern for UserUsableGroups and TopupGroupRatio.
Do you want me to draft the helpers and usages across the three sync functions?

Also applies to: 295-321, 322-344

web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (4)

201-214: Use functional setState to avoid stale state on rapid toggles.

Prevents race conditions when multiple updates queue.

-  function handleSectionChange(sectionKey) {
+  function handleSectionChange(sectionKey) {
     return (checked) => {
-      const newModules = {
-        ...sidebarModulesUser,
-        [sectionKey]: {
-          ...sidebarModulesUser[sectionKey],
-          enabled: checked,
-        },
-      };
-      setSidebarModulesUser(newModules);
+      setSidebarModulesUser(prev => ({
+        ...prev,
+        [sectionKey]: {
+          ...prev[sectionKey],
+          enabled: checked,
+        },
+      }));
     };
   }
-  function handleModuleChange(sectionKey, moduleKey) {
+  function handleModuleChange(sectionKey, moduleKey) {
     return (checked) => {
-      // 在个人设置中,所有模块都使用简单布尔值,不处理嵌套对象的子功能
-      const newModules = {
-        ...sidebarModulesUser,
-        [sectionKey]: {
-          ...sidebarModulesUser[sectionKey],
-          [moduleKey]: checked
-        }
-      };
-      setSidebarModulesUser(newModules);
+      // 在个人设置中,所有模块都使用简单布尔值,不处理嵌套对象的子功能
+      setSidebarModulesUser(prev => ({
+        ...prev,
+        [sectionKey]: {
+          ...prev[sectionKey],
+          [moduleKey]: checked,
+        },
+      }));
     };
   }

Also applies to: 216-236


281-297: Parse setting JSON once; remove duplicate JSON.parse and reuse local variables.

Slight perf/readability win; reduces double parsing and drift.

-        if (userRes.data.success && userRes.data.data.setting) {
-          // 从setting字段中获取系统权限信息
-          try {
-            const setting = JSON.parse(userRes.data.data.setting);
-            if (setting.sidebar_system_config) {
-              const systemConfig = JSON.parse(setting.sidebar_system_config);
-              setAdminConfig(systemConfig);
-            } else {
-              // 如果没有系统配置,使用默认配置
-              setAdminConfig(getDefaultSystemConfig());
-              console.log('使用默认系统配置');
-            }
-          } catch (error) {
-            console.error('解析系统配置失败:', error);
-            setAdminConfig(getDefaultSystemConfig());
-          }
-
-          // 从同一个setting字段中获取用户的原始偏好设置
-          try {
-            const setting = JSON.parse(userRes.data.data.setting);
-            const systemConfig = setting.sidebar_system_config ? JSON.parse(setting.sidebar_system_config) : getDefaultSystemConfig();
+        if (userRes.data.success && userRes.data.data.setting) {
+          try {
+            const settingStr = userRes.data.data.setting;
+            const setting = JSON.parse(settingStr);
+            const systemConfig = setting.sidebar_system_config
+              ? JSON.parse(setting.sidebar_system_config)
+              : getDefaultSystemConfig();
+            setAdminConfig(systemConfig);
             if (setting.sidebar_modules) {
               let userConf;
               if (typeof setting.sidebar_modules === 'string') {
                 userConf = JSON.parse(setting.sidebar_modules);
               } else {
                 userConf = setting.sidebar_modules;
               }
-              // 确保用户配置包含所有系统允许的区域,即使用户关闭了它们
               const mergedConfig = mergeUserConfigWithSystemConfig(userConf, systemConfig);
               setSidebarModulesUser(mergedConfig);
             } else {
               const defaultConfig = generateDefaultConfig(systemConfig);
               setSidebarModulesUser(defaultConfig);
             }
-          } catch (error) {
-            console.error('解析用户设置失败:', error);
-            const defaultConfig = generateDefaultConfig();
-            setSidebarModulesUser(defaultConfig);
-          }
+          } catch (error) {
+            console.error('解析系统/用户配置失败:', error);
+            setAdminConfig(getDefaultSystemConfig());
+            setSidebarModulesUser(generateDefaultConfig());
+          }

Also applies to: 298-322


410-411: Remove or gate debug logs to avoid noisy console in production.

Wrap logs behind a dev flag or remove entirely.

-  console.log(`区域 ${section.key} 系统是否允许:`, systemAllowed, 'adminConfig:', adminConfig);
+  // dev-only debug:
+  // if (process.env.NODE_ENV !== 'production') console.log('section allowed', section.key, systemAllowed);
-  console.log(`模块 ${section.key}.${module.key} 系统是否允许:`, allowed);
+  // if (process.env.NODE_ENV !== 'production') console.log('module allowed', `${section.key}.${module.key}`, allowed);
-  console.log(`区域 ${section.key} 是否有可用模块:`, hasModules, '模块数量:', section.modules.length);
+  // if (process.env.NODE_ENV !== 'production') console.log('section has modules', section.key, hasModules, section.modules.length);
-  console.log('用户边栏区域配置变更:', sectionKey, checked, newModules);
+  // if (process.env.NODE_ENV !== 'production') console.log('section change', sectionKey, checked);
-  console.log(
-    '用户边栏功能配置变更:',
-    sectionKey,
-    moduleKey,
-    checked,
-    newModules,
-  );
+  // if (process.env.NODE_ENV !== 'production') console.log('module change', sectionKey, moduleKey, checked);
-  console.log('用户边栏配置重置为默认:', defaultConfig);
+  // if (process.env.NODE_ENV !== 'production') console.log('reset to default');
-  console.log('保存用户边栏配置:', sidebarModulesUser);
+  // if (process.env.NODE_ENV !== 'production') console.log('saving sidebar config');
-  console.log('用户边栏配置保存成功');
+  // if (process.env.NODE_ENV !== 'production') console.log('save success');
-  await refreshUserConfig();
-  console.log('用户边栏配置已刷新,边栏将立即更新');
+  await refreshUserConfig();
+  // if (process.env.NODE_ENV !== 'production') console.log('sidebar refreshed');

Also applies to: 418-419, 423-425, 212-213, 228-235, 243-244, 257-262


79-111: Avoid recreating default config on every render.

Move getDefaultSystemConfig outside the component or memoize with useMemo to reduce allocations.

Example:

-  const getDefaultSystemConfig = () => {
+  const getDefaultSystemConfig = useMemo(() => ({
     // ...
-  };
+  }), []);
controller/user.go (2)

681-715: Default system config: OK, but consider centralizing.
Looks fine. Optionally move to a config/consts package to avoid controller coupling and enable reuse in model/tests.


725-755: Module-level permission rules: OK; consider decl-based rules.
Rules read well. Optionally move exceptions (console.detail, admin.setting, admin.user.groupManagement) into a declarative map to avoid stringly-typed paths.

📜 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 8afd55d and 53ecf63.

📒 Files selected for processing (47)
  • common/topup-ratio.go (3 hunks)
  • controller/misc.go (4 hunks)
  • controller/pricing.go (3 hunks)
  • controller/user.go (10 hunks)
  • controller/user_group.go (1 hunks)
  • middleware/auth.go (2 hunks)
  • model/main.go (3 hunks)
  • model/option.go (1 hunks)
  • model/user.go (7 hunks)
  • model/user_group.go (1 hunks)
  • router/api-router.go (7 hunks)
  • setting/operation_setting/general_setting.go (1 hunks)
  • web/index.html (1 hunks)
  • web/src/App.jsx (6 hunks)
  • web/src/components/auth/AuthPageLayout.jsx (1 hunks)
  • web/src/components/auth/LoginForm.jsx (3 hunks)
  • web/src/components/auth/ModuleRoute.jsx (1 hunks)
  • web/src/components/auth/PasswordResetConfirm.jsx (3 hunks)
  • web/src/components/auth/PasswordResetForm.jsx (3 hunks)
  • web/src/components/auth/RegisterForm.jsx (3 hunks)
  • web/src/components/layout/Footer.jsx (1 hunks)
  • web/src/components/layout/PageLayout.jsx (1 hunks)
  • web/src/components/layout/headerbar/LanguageSelector.jsx (1 hunks)
  • web/src/components/settings/OtherSetting.jsx (1 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (6 hunks)
  • web/src/components/settings/personal/components/UserInfoHeader.jsx (2 hunks)
  • web/src/components/table/users/UsersActions.jsx (1 hunks)
  • web/src/components/table/users/index.jsx (1 hunks)
  • web/src/components/table/users/modals/AddUserModal.jsx (4 hunks)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
  • web/src/components/topup/index.jsx (6 hunks)
  • web/src/constants/user.constants.js (1 hunks)
  • web/src/helpers/api.js (2 hunks)
  • web/src/helpers/data.js (1 hunks)
  • web/src/helpers/render.jsx (2 hunks)
  • web/src/helpers/utils.jsx (1 hunks)
  • web/src/hooks/common/useHeaderBar.js (1 hunks)
  • web/src/hooks/common/useSidebar.js (3 hunks)
  • web/src/hooks/common/useUserPermissions.js (1 hunks)
  • web/src/hooks/model-pricing/useModelPricingData.jsx (2 hunks)
  • web/src/i18n/locales/en.json (4 hunks)
  • web/src/pages/Home/index.jsx (2 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (3 hunks)
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1 hunks)
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (8 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (34)
  • web/src/components/table/users/index.jsx
  • web/src/helpers/render.jsx
  • web/src/components/layout/headerbar/LanguageSelector.jsx
  • web/src/hooks/common/useUserPermissions.js
  • web/src/components/auth/AuthPageLayout.jsx
  • setting/operation_setting/general_setting.go
  • web/src/components/auth/RegisterForm.jsx
  • web/src/components/table/users/modals/EditUserGroupModal.jsx
  • model/option.go
  • web/src/components/layout/Footer.jsx
  • web/src/helpers/data.js
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx
  • web/src/components/settings/OtherSetting.jsx
  • web/src/components/table/users/modals/AddUserModal.jsx
  • web/src/components/auth/PasswordResetConfirm.jsx
  • web/src/components/settings/personal/components/UserInfoHeader.jsx
  • web/src/helpers/utils.jsx
  • controller/pricing.go
  • web/src/components/layout/PageLayout.jsx
  • web/src/constants/user.constants.js
  • web/src/helpers/api.js
  • web/src/pages/Home/index.jsx
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/auth/ModuleRoute.jsx
  • middleware/auth.go
  • router/api-router.go
  • web/index.html
  • web/src/components/table/users/UsersActions.jsx
  • web/src/hooks/common/useHeaderBar.js
  • model/user.go
  • model/user_group.go
  • web/src/i18n/locales/en.json
  • web/src/components/settings/personal/cards/NotificationSettings.jsx
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-02T16:17:53.666Z
Learnt from: x-Ai
PR: QuantumNous/new-api#1703
File: middleware/auth.go:264-293
Timestamp: 2025-09-02T16:17:53.666Z
Learning: The sidebar management system introduced in this codebase uses SidebarModulesAdmin configuration to control admin user permissions. Admin access to console.* modules should be governed by this configuration system, not bypassed with hardcoded allowlists. The system is designed for granular permission control where system administrators can configure which features admin users can access.

Applied to files:

  • web/src/hooks/common/useSidebar.js
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
🧬 Code graph analysis (13)
web/src/components/table/users/modals/UserGroupManagement.jsx (2)
web/src/components/table/users/modals/EditUserGroupModal.jsx (8)
  • useTranslation (41-41)
  • loading (43-43)
  • sidebarLoading (44-44)
  • getUserRole (47-50)
  • isRoot (52-52)
  • isAdmin (53-53)
  • hasGroupManagementPermission (56-83)
  • EditUserGroupModal (40-275)
web/src/hooks/common/useSidebar.js (4)
  • loading (32-32)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
controller/misc.go (2)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/constants.go (4)
  • OptionMap (36-36)
  • RoleRootUser (136-136)
  • RoleAdminUser (135-135)
  • RoleCommonUser (134-134)
model/main.go (2)
model/user_group.go (2)
  • UserGroup (8-16)
  • InitDefaultUserGroups (103-137)
common/sys_log.go (1)
  • SysLog (10-13)
controller/user_group.go (8)
model/user_group.go (5)
  • GetAllUserGroups (51-55)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (78-86)
  • GetUserGroupById (58-65)
  • IsUserGroupInUse (89-93)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-49)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
common/topup-ratio.go (2)
  • GetTopupGroupRatioCopy (16-24)
  • UpdateTopupGroupRatioByJSONString (36-45)
common/topup-ratio.go (1)
common/json.go (1)
  • Unmarshal (8-10)
web/src/hooks/model-pricing/useModelPricingData.jsx (2)
web/src/hooks/models/useModelsData.jsx (1)
  • vendorMap (101-107)
web/src/helpers/utils.jsx (1)
  • showError (118-147)
web/src/hooks/common/useSidebar.js (2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
  • useSidebar (55-55)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • useSidebar (100-100)
controller/user.go (4)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
dto/user_settings.go (1)
  • UserSetting (3-13)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleAdminUser (135-135)
model/user.go (1)
  • GenerateDefaultSidebarConfigForRole (96-161)
web/src/components/auth/PasswordResetForm.jsx (1)
web/src/components/auth/AuthPageLayout.jsx (1)
  • AuthPageLayout (22-37)
web/src/components/topup/index.jsx (2)
web/src/components/topup/InvitationCard.jsx (1)
  • InvitationCard (34-227)
web/src/helpers/render.jsx (1)
  • renderQuota (907-925)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • handleModuleChange (116-127)
web/src/App.jsx (4)
web/src/hooks/common/useHeaderBar.js (3)
  • headerNavModulesConfig (55-55)
  • statusState (34-34)
  • location (40-40)
web/src/components/auth/ModuleRoute.jsx (2)
  • statusState (19-19)
  • ModuleRoute (17-147)
web/src/helpers/auth.jsx (1)
  • AdminRoute (52-66)
web/src/components/common/ui/Loading.jsx (1)
  • Loading (23-29)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)
web/src/components/settings/personal/cards/NotificationSettings.jsx (5)
  • sidebarModulesUser (60-87)
  • adminConfig (88-88)
  • permissionsLoading (91-97)
  • getDefaultSystemConfig (174-203)
  • handleModuleChange (116-127)
web/src/hooks/common/useUserPermissions.js (1)
  • hasSidebarSettingsPermission (58-60)
🪛 Biome (2.1.2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

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

Hooks should not be called after an early return.

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)

🔇 Additional comments (29)
web/src/components/auth/PasswordResetForm.jsx (2)

34-34: AuthPageLayout import improves consistency — nice.

Centralizing auth page chrome via a shared layout is a good move and the import path looks correct.


108-110: Verify min-h-full works in your app shell; consider min-h-screen if not.

AuthPageLayout uses min-h-full; if html, body, and #root aren’t 100% height, vertical centering can break. Either ensure those globals are h-full or switch AuthPageLayout to min-h-screen.

Also applies to: 181-182

web/src/hooks/model-pricing/useModelPricingData.jsx (2)

22-22: LGTM! Good addition of navigation capability.

Adding useNavigate for handling authentication/authorization errors is a sensible approach for better user experience.

Also applies to: 30-30


232-241: Good error handling for 403 responses.

The differentiation between unauthenticated (redirect to /login) and unauthorized (redirect to /forbidden) users is well implemented. This aligns with the backend's module-based authorization system.

web/src/components/topup/index.jsx (3)

63-64: Good initialization to prevent UI flicker.

Starting with invitationEnabled as false prevents the invitation card from flashing before the configuration loads.


519-521: Good responsive layout implementation.

The conditional grid layout and column spans based on invitationConfigLoaded && invitationEnabled properly adapt the UI. The approach prevents layout shifts and provides a clean responsive design.

Also applies to: 555-567


333-333: Clean consolidation of invitation link fetching.

Good refactoring to centralize the invitation link fetching through getInvitationConfig, removing the duplicate call and ensuring it only happens when the feature is enabled.

Also applies to: 381-381

web/src/App.jsx (4)

25-25: LGTM! Proper module-based authorization added.

Good addition of ModuleRoute import to enable fine-grained module access control.


62-80: Correct update to match backend field name.

The change from HeaderNavModules to header_nav_modules properly aligns with the backend's snake_case naming convention.


106-108: Comprehensive module-based access control implementation.

Good systematic wrapping of all admin and console routes with ModuleRoute components. This provides consistent module-level permission checking across the application.

Also applies to: 116-118, 126-128, 136-138, 146-148, 156-158


272-276: Dashboard access is unconditionally allowed. ModuleRoute’s checkModulePermissionInConfig returns true for "console.detail", so the dashboard remains accessible to all authenticated users—no changes needed.

model/main.go (1)

306-307: LGTM on including UserGroup in fast migrations.

controller/misc.go (8)

45-49: User role extraction is correct and uses -1 for anonymous.


76-76: Invitation feature flag exposed via status payload looks good.


101-103: Header nav now filtered server-side per role. Good defense-in-depth.


369-489: Admin sidebar filtering is comprehensive and defaults safe for anonymous.

Logic for sections, nested modules, and console.detail exemption reads correctly.


491-506: Module enabled checks handle bool and object forms correctly.


530-560: Pricing permission logic is sound (enabled + requireAuth).


562-593: Role-tiered checks for nested modules look correct; root bypasses.


595-619: User allowlist is explicit. Verify these defaults match desired exposure.

Confirm that normal users should see items like console.log and console.midjourney by default. Adjust map if needed.

common/topup-ratio.go (1)

14-56: Thread-safety fixes look solid.

RWMutex usage, atomic swap on update, and safe copy eliminate races on TopupGroupRatio.

controller/user.go (8)

13-13: Import for operation settings gating looks good.
No issues; used consistently below.


379-387: Invite feature-flag gate for quota transfer: LGTM.
Early return with clear UX message; avoids side effects when disabled.


415-423: Invite feature-flag gate for aff code retrieval: LGTM.
Consistent with TransferAffQuota.


491-494: Permissions slimming looks reasonable.
Only exposing sidebar_settings minimizes payload and ambiguity.


716-724: Section-level permission check: OK.
Straightforward guard for admin area.


1358-1361: Default group assignment and persistence: LGTM.
Sensible default and preserved in inserted user.

Also applies to: 1376-1376


1471-1478: Sync sidebar config on role change: LGTM.
Uses GenerateDefaultSidebarConfigForRole and persists via Update later in the function; audit log is helpful.

Also applies to: 1496-1503


485-489: Verify client handling of setting response type.
cleanUserSettingForResponse now returns setting as a JSON‐encoded string; front-end code (e.g. checks on res.data.data.setting) appears to treat it as an object. Confirm all clients JSON.parse or otherwise handle the string, or revert to returning an object to avoid breaking changes.

Comment thread controller/user_group.go
Comment thread controller/user.go
Comment thread controller/user.go
Comment thread controller/user.go
Comment thread model/main.go
Comment thread web/src/components/table/users/modals/UserGroupManagement.jsx
Comment thread web/src/hooks/common/useSidebar.js
Comment thread web/src/hooks/common/useSidebar.js
Comment thread web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
Comment thread web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

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

🧹 Nitpick comments (4)
controller/user_group.go (4)

50-51: Drop redundant strings.ToLower; helper normalizes internally.
Simplifies calls and avoids double-allocation.

Apply:

- if isReservedGroup(strings.ToLower(g.Name)) {
+ if isReservedGroup(g.Name) {
- if isReservedGroup(strings.ToLower(g.Name)) {
+ if isReservedGroup(g.Name) {
- if isReservedGroup(strings.ToLower(oldGroup.Name)) {
+ if isReservedGroup(oldGroup.Name) {

Also applies to: 155-156, 163-164


286-293: Avoid magic strings for option keys; centralize as constants.
Prevents typos and eases refactors across packages.

Example (define near top or in a shared settings/constants pkg):

+const (
+  optKeyGroupRatio      = "GroupRatio"
+  optKeyUserUsableGroups= "UserUsableGroups"
+  optKeyTopupGroupRatio = "TopupGroupRatio"
+)

Then:

- if err := model.UpdateOption("GroupRatio", string(jsonBytes)); err != nil {
+ if err := model.UpdateOption(optKeyGroupRatio, string(jsonBytes)); err != nil {
- if err := model.UpdateOption("UserUsableGroups", string(jsonBytes)); err != nil {
+ if err := model.UpdateOption(optKeyUserUsableGroups, string(jsonBytes)); err != nil {
- if err := model.UpdateOption("TopupGroupRatio", string(jsonBytes)); err != nil {
+ if err := model.UpdateOption(optKeyTopupGroupRatio, string(jsonBytes)); err != nil {

Also applies to: 313-320, 337-344


187-215: Consider eventual-consistency hardening for post-commit syncs.
Syncs to three subsystems happen after commit and are best-effort. Add a lightweight retry, DLQ, or periodic reconciler to heal drift.


233-251: Guard against TOCTOU on delete-or-in-use.
Between IsUserGroupInUse() and Delete(), assignments could race. Prefer a DB constraint (FK) or perform the check and delete in a transaction with appropriate locks; alternatively, rely on FK and surface constraint violation.

📜 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 53ecf63 and 9f53145.

📒 Files selected for processing (1)
  • controller/user_group.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
controller/user_group.go (8)
model/user_group.go (5)
  • GetAllUserGroups (51-55)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (78-86)
  • GetUserGroupById (58-65)
  • IsUserGroupInUse (89-93)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-49)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
common/topup-ratio.go (2)
  • GetTopupGroupRatioCopy (16-24)
  • UpdateTopupGroupRatioByJSONString (36-45)
🔇 Additional comments (1)
controller/user_group.go (1)

17-25: LGTM on list endpoint.
Straightforward fetch + error handling; consistent with model API.

Comment thread controller/user_group.go
Comment thread controller/user_group.go

@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

Caution

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

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

213-220: Bug: closes the wrong modal after password change.

setShowWeChatBindModal(false) should be setShowChangePasswordModal(false); currently the WeChat modal is toggled instead of the password modal.

Apply:

-    if (success) {
-      showSuccess(t('密码修改成功!'));
-      setShowWeChatBindModal(false);
-    } else {
+    if (success) {
+      showSuccess(t('密码修改成功!'));
+      setShowChangePasswordModal(false);
+    } else {
       showError(message);
     }
-    setShowChangePasswordModal(false);
+    // ensure closed if success path above didn't run
+    // setShowChangePasswordModal(false);

256-260: State mutation bug: directly mutating context user.

userState.user.email = ... mutates state outside the reducer; this can cause stale renders.

Use dispatch or refresh:

-      setShowEmailBindModal(false);
-      userState.user.email = inputs.email;
+      setShowEmailBindModal(false);
+      await getUserData(); // or dispatch immutable update:
+      // userDispatch({ type: 'login', payload: { ...userState.user, email: inputs.email } });

227-235: Cooldown starts even when Turnstile isn’t ready (premature disable).

You call setDisableButton(true) before checking Turnstile; early-return keeps the button disabled and starts the countdown although no code was sent.

Start cooldown only on successful send (or at least after readiness check):

-    setDisableButton(true);
-    if (turnstileEnabled && turnstileToken === '') {
+    if (turnstileEnabled && turnstileToken === '') {
       showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
       return;
     }
+    setDisableButton(true);

Optional: also move setDisableButton(true) into the success branch to cooldown only when email actually sent.

model/user.go (2)

421-439: Inviter reward is logged but not applied when QuotaForInviter > 0.
This will silently skip quota crediting while emitting a reward log. Restore the quota increase for inviter inside the >0 block.

Apply this diff:

-      if common.QuotaForInviter > 0 {
-        //_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
-        RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
-      }
+      if common.QuotaForInviter > 0 {
+        _ = IncreaseUserQuota(inviterId, common.QuotaForInviter, true)
+        RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
+      }

735-765: Return-type bug: GetUserSetting returns string instead of dto.UserSetting for Redis hit.
This won’t compile and/or will return wrong type. Convert the cached JSON to UserSetting before returning.

Apply this diff:

-  if !fromDB && common.RedisEnabled {
-    setting, err := getUserSettingCache(id)
-    if err == nil {
-      return setting, nil
-    }
-  }
+  if !fromDB && common.RedisEnabled {
+    cached, err := getUserSettingCache(id)
+    if err == nil {
+      ub := &UserBase{Setting: cached}
+      return ub.GetSetting(), nil
+    }
+  }
controller/user.go (1)

169-177: Don’t return inline avatar data from setupLogin.
Login responses can become multi-MB. Align with GetSelf: omit avatar or set to empty and rely on GetUserAvatar.

Apply this diff:

-    Avatar:      user.Avatar,
+    Avatar:      "",
♻️ Duplicate comments (2)
controller/user.go (2)

819-857: Avoid interface-to-bool equality; use typed assertion for “enabled.”
Same issue noted previously; switch to explicit bool extraction.

Apply this diff:

-    systemSectionEnabled, hasEnabled := sectionObj["enabled"]
-    if !hasEnabled || systemSectionEnabled != true {
+    sectionEnabled := true
+    if v, has := sectionObj["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

953-962: Same typed-bool concern in filters and final display calc.
Replace systemSection["enabled"] != true with typed bool extraction.

Apply this diff:

-    if !ok || systemSection["enabled"] != true {
+    if !ok {
+      continue
+    }
+    sectionEnabled := true
+    if v, has := systemSection["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

Use the same pattern in calculateFinalDisplayConfig.

Also applies to: 1037-1041

🧹 Nitpick comments (15)
web/src/components/layout/headerbar/UserArea.jsx (1)

123-128: Avoid potential crash on empty username; add alt for accessibility.

Guard the initial extraction and provide alt text. Also make color fallback resilient.

-            src={userState.user?.avatar || undefined}
-            color={userState.user?.avatar ? undefined : stringToColor(userState.user.username)}
+            src={userState.user?.avatar || undefined}
+            color={userState.user?.avatar ? undefined : stringToColor(userState.user?.username || '')}
+            alt={userState.user?.username || 'user'}
           >
-            {!userState.user?.avatar && userState.user.username[0].toUpperCase()}
+            {!userState.user?.avatar && userState.user?.username?.[0]?.toUpperCase?.()}
web/src/helpers/avatarCache.js (1)

25-70: Add cache invalidation (TTL/version) to prevent stale avatars.

Currently, cached avatars never refresh unless logout/explicit clear. Introduce TTL or versioning to auto-bust stale entries.

Example approach (outline):

  • Store { data, ts } JSON and define MAX_AGE_MS (e.g., 7 days).
  • On get, if Date.now() - ts > MAX_AGE_MS, treat as miss and clear.
  • Optionally incorporate a server-provided avatarVersion to key the cache.
web/src/i18n/locales/en.json (1)

2004-2005: Improve English phrasing.

"Control header module display status, global effect" → clearer wording.

-  "控制导航栏模块显示状态,全局生效": "Control header module display status, global effect",
+  "控制导航栏模块显示状态,全局生效": "Control which navigation bar modules are visible (applies globally)",
web/src/components/settings/PersonalSetting.jsx (2)

85-87: Mount-time refresh is fine.

Consider also guarding against unmount with a flag if you later add async chains here.


90-101: Countdown effect can be simplified and avoid interval churn.

Use a single timeout with functional state update to avoid capturing stale countdown and repeated interval setup/cleanup.

-  useEffect(() => {
-    let countdownInterval = null;
-    if (disableButton && countdown > 0) {
-      countdownInterval = setInterval(() => {
-        setCountdown(countdown - 1);
-      }, 1000);
-    } else if (countdown === 0) {
-      setDisableButton(false);
-      setCountdown(30);
-    }
-    return () => clearInterval(countdownInterval); // Clean up on unmount
-  }, [disableButton, countdown]);
+  useEffect(() => {
+    if (!disableButton) return;
+    if (countdown === 0) {
+      setDisableButton(false);
+      setCountdown(30);
+      return;
+    }
+    const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
+    return () => clearTimeout(timer);
+  }, [disableButton, countdown]);
model/user.go (4)

96-161: Default sidebar config generation is sensible; consider deduping with controller defaults.
To avoid drift, centralize default structure (e.g., single source used by both model.GenerateDefaultSidebarConfigForRole and controller getDefaultSystemConfig).


195-226: Avoid wrapping pure reads in explicit transactions.
These reads don’t require a transaction and the extra tx adds overhead and contention. Consider direct queries with consistent read options.


233-293: Same as above: drop the read transaction unless you need snapshot semantics.


933-1000: Group rename: handle no-op updates and tighten cache refresh.

  • Return a clear error when result.RowsAffected == 0 to surface “old group not found.”
  • Optionally batch cache invalidation to reduce Redis round-trips.

Apply this diff to surface no-op updates:

   result := tx.Model(&User{}).Where(commonGroupCol+" = ?", oldGroupName).Update(commonGroupCol, newGroupName)
   if result.Error != nil {
     tx.Rollback()
     common.SysLog(fmt.Sprintf("更新用户分组名称失败: %s", result.Error.Error()))
     return result.Error
   }
+  if result.RowsAffected == 0 {
+    tx.Rollback()
+    return fmt.Errorf("未找到使用分组名 '%s' 的用户", oldGroupName)
+  }

Run to verify behavior on no-op rename in a dev DB before merging.

web/src/components/settings/personal/components/UserInfoHeader.jsx (3)

42-58: Inject styles in a useEffect to avoid running at module import time.
Prevents unexpected behavior in SSR/bundlers and avoids duplicate work.

Apply this diff:

-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
@@
-// 注入样式
-if (typeof document !== 'undefined') {
-  const styleElement = document.createElement('style');
-  styleElement.textContent = avatarUploadStyle;
-  if (!document.head.querySelector('style[data-avatar-upload]')) {
-    styleElement.setAttribute('data-avatar-upload', 'true');
-    document.head.appendChild(styleElement);
-  }
-}
+// 组件挂载后再注入样式
+useEffect(() => {
+  if (typeof document === 'undefined') return;
+  if (!document.head.querySelector('style[data-avatar-upload]')) {
+    const styleElement = document.createElement('style');
+    styleElement.textContent = avatarUploadStyle;
+    styleElement.setAttribute('data-avatar-upload', 'true');
+    document.head.appendChild(styleElement);
+  }
+}, []);

128-164: Base64 size check is inaccurate; compute decoded bytes.
Using string length can under/over-estimate. Compute bytes from the base64 payload.

Apply this diff:

-        // 验证base64数据大小(约2MB限制)
-        const base64Size = base64Data.length;
-        const maxBase64Size = 2 * 1024 * 1024; // 2MB
-        if (base64Size > maxBase64Size) {
+        // 验证base64数据大小(解码后≤2MB)
+        const b64 = (base64Data.split(',')[1] || '');
+        const padding = b64.endsWith('==') ? 2 : (b64.endsWith('=') ? 1 : 0);
+        const decodedBytes = Math.floor((b64.length * 3) / 4) - padding;
+        const maxBytes = 2 * 1024 * 1024; // 2MB
+        if (decodedBytes > maxBytes) {
           Toast.error(t('图片编码后数据过大,请选择更小的图片'));
           return;
         }

90-93: Remove console debugging logs before release.

-    console.log('beforeUpload file:', file); // 调试日志
+    // console.debug('beforeUpload file:', file);
@@
-    console.log('handleFileChange currentFile:', currentFile); // 调试日志
+    // console.debug('handleFileChange currentFile:', currentFile);
@@
-        console.error('File processing error:', error);
+        // console.error('File processing error:', error);

Also applies to: 130-131, 158-160

controller/user.go (3)

31-85: Server-side avatar validation is solid; consider minor robustness tweaks.
You can accept case-insensitive MIME and tolerate extra parameters (e.g., “;charset=utf-8”) by parsing via a regex for ^data:image/(jpeg|jpg|png|gif|webp);base64$.


1099-1128: Nested module user prefs aren’t merged; child toggles are ignored.
Users can’t override submodules (e.g., admin.user.groupManagement). Merge child-level booleans with system defaults while respecting parent enabled.

Apply this diff:

-        // 合并系统配置和用户偏好
-        finalNestedObj := make(map[string]interface{})
-        for k, v := range nestedObj {
-          if k == "enabled" {
-            finalNestedObj[k] = effectiveEnabled
-          } else {
-            // 其他字段保持系统配置
-            finalNestedObj[k] = v
-          }
-        }
-
-        // 如果区域被禁用,强制将嵌套对象的enabled设置为false
-        if !userSectionEnabled {
-          finalNestedObj["enabled"] = false
-        }
+        // 合并系统配置与用户偏好(包含子功能布尔覆盖)
+        parentEnabled := true
+        if b, ok := effectiveEnabled.(bool); ok {
+          parentEnabled = b
+        }
+        finalNestedObj := make(map[string]interface{})
+        for k, v := range nestedObj {
+          if k == "enabled" {
+            finalNestedObj[k] = parentEnabled && userSectionEnabled
+            continue
+          }
+          switch sv := v.(type) {
+          case bool:
+            userAllow := true
+            if uv, ok := userNestedObj[k]; ok {
+              if ub, ok := uv.(bool); ok {
+                userAllow = ub
+              }
+            }
+            finalNestedObj[k] = sv && userAllow && parentEnabled && userSectionEnabled
+          default:
+            finalNestedObj[k] = v
+          }
+        }

1227-1273: Simplify GetUserAvatar; drop header-based “caching” knobs.
The X-Session-ID/X-Avatar-Fetched dance doesn’t persist server-side and adds complexity without benefit. Always return avatar; let the client/cache handle memoization (or add proper ETag/Last-Modified later).

Apply this diff:

-  // 检查是否强制获取头像(用于上传后刷新)
-  forceRefresh := c.Query("force_refresh") == "true"
-
-  // 检查会话中是否已获取过头像
-  sessionId := c.GetHeader("X-Session-ID")
-  if sessionId == "" {
-    // 如果没有会话ID,生成一个
-    sessionId = fmt.Sprintf("session_%d_%d", id, time.Now().Unix())
-  }
-
-  // 如果不是强制刷新且会话中已获取过,返回空响应
-  if !forceRefresh {
-    if sessionValue := c.GetHeader("X-Avatar-Fetched"); sessionValue == "true" {
-      c.JSON(http.StatusOK, gin.H{
-        "success": true,
-        "message": "avatar_cached",
-        "data": gin.H{
-          "avatar": "",
-          "cached": true,
-        },
-      })
-      return
-    }
-  }
-
-  // 返回头像数据
+  // 返回头像数据
   c.JSON(http.StatusOK, gin.H{
     "success": true,
     "message": "",
     "data": gin.H{
       "avatar": user.Avatar,
-      "cached": false,
-      "session_id": sessionId,
+      "cached": false,
     },
   })

Confirm no frontend relies on these headers; the new UserInfoHeader and helpers appear to ignore them.

📜 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 9f53145 and acd5492.

📒 Files selected for processing (12)
  • controller/user.go (15 hunks)
  • model/user.go (9 hunks)
  • router/api-router.go (8 hunks)
  • web/src/components/layout/headerbar/UserArea.jsx (1 hunks)
  • web/src/components/settings/PersonalSetting.jsx (4 hunks)
  • web/src/components/settings/personal/components/UserInfoHeader.jsx (5 hunks)
  • web/src/context/User/reducer.js (1 hunks)
  • web/src/helpers/avatarCache.js (1 hunks)
  • web/src/helpers/userDataManager.js (1 hunks)
  • web/src/hooks/dashboard/useDashboardData.js (1 hunks)
  • web/src/i18n/locales/en.json (4 hunks)
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx
🧰 Additional context used
🧬 Code graph analysis (9)
web/src/context/User/reducer.js (1)
web/src/helpers/userDataManager.js (2)
  • cleanupOnLogout (106-116)
  • cleanupOnLogout (106-116)
web/src/components/layout/headerbar/UserArea.jsx (1)
web/src/helpers/render.jsx (1)
  • stringToColor (561-568)
web/src/helpers/userDataManager.js (2)
web/src/hooks/dashboard/useDashboardData.js (1)
  • getUserData (216-230)
web/src/helpers/avatarCache.js (7)
  • getCachedAvatar (32-40)
  • getCachedAvatar (32-40)
  • cacheAvatar (47-58)
  • cacheAvatar (47-58)
  • avatarData (34-34)
  • clearAvatarCache (64-70)
  • clearAvatarCache (64-70)
web/src/hooks/dashboard/useDashboardData.js (2)
web/src/helpers/userDataManager.js (2)
  • getUserData (35-70)
  • getUserData (35-70)
web/src/helpers/utils.jsx (1)
  • showError (118-147)
web/src/components/settings/PersonalSetting.jsx (3)
web/src/helpers/userDataManager.js (2)
  • getUserData (35-70)
  • getUserData (35-70)
web/src/hooks/dashboard/useDashboardData.js (1)
  • getUserData (216-230)
web/src/components/settings/personal/components/UserInfoHeader.jsx (1)
  • UserInfoHeader (59-425)
web/src/components/settings/personal/components/UserInfoHeader.jsx (2)
web/src/helpers/render.jsx (1)
  • stringToColor (561-568)
web/src/helpers/userDataManager.js (2)
  • updateUserAvatar (78-100)
  • updateUserAvatar (78-100)
controller/user.go (6)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
dto/user_settings.go (1)
  • UserSetting (3-13)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleAdminUser (135-135)
model/user.go (3)
  • GetUserById (295-307)
  • User (20-50)
  • GenerateDefaultSidebarConfigForRole (97-162)
common/gin.go (1)
  • ApiError (95-100)
common/sys_log.go (1)
  • SysLog (10-13)
router/api-router.go (8)
controller/user.go (1)
  • GetUserAvatar (1227-1273)
middleware/auth.go (3)
  • ModuleAuth (180-223)
  • AdminAuth (163-167)
  • UserAuth (157-161)
controller/log.go (3)
  • DeleteHistoryLogs (148-168)
  • GetLogsStat (101-122)
  • GetLogsSelfStat (124-146)
controller/usedata.go (1)
  • GetUserQuotaDates (29-52)
controller/user_group.go (4)
  • GetAllUserGroups (18-25)
  • CreateUserGroup (38-92)
  • UpdateUserGroup (95-217)
  • DeleteUserGroup (220-269)
model/user_group.go (1)
  • GetAllUserGroups (51-55)
controller/midjourney.go (2)
  • GetUserMidjourney (271-294)
  • GetAllMidjourney (246-269)
controller/task.go (2)
  • GetUserTask (252-274)
  • GetAllTask (229-250)
model/user.go (7)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
common/constants.go (2)
  • QuotaForInvitee (100-100)
  • QuotaForInviter (99-99)
model/log.go (1)
  • RecordLog (78-94)
logger/logger.go (1)
  • LogQuota (94-100)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
common/redis.go (1)
  • RedisEnabled (17-17)
🔇 Additional comments (27)
web/src/i18n/locales/en.json (1)

2139-2143: Ignore size inconsistency suggestion
The 1.5 MB message is used for raw-file validation and the ~2 MB message is for Base64-encoded payload—both limits are intentional and correctly reflected in code.

Likely an incorrect or invalid review comment.

web/src/components/settings/PersonalSetting.jsx (4)

26-26: Good decoupling: centralize user fetch via helper.

Import aliasing avoids shadowing the local getUserData and improves reuse.


136-149: Helper-wrapped getUserData: solid error handling.

Consistent with hooks usage; OK to dispatch on success and toast on failure.


319-319: Passing onUserDataUpdate is a nice touch.

Lets the header refresh parent state after avatar updates.


312-313: No-op whitespace.

Nothing to review here.

web/src/helpers/userDataManager.js (2)

35-70: Centralized user fetch with avatar caching: LGTM.

Cache-first avatar retrieval with a graceful fallback is clear and resilient.


106-116: Logout cleanup looks good.

Clears per-user avatar cache safely.

router/api-router.go (4)

58-58: Add /api/user/avatar: good placement under authenticated selfRoute.

Matches the helper’s GET path and keeps scope limited to logged-in users.


83-83: Consistent per-module authorization layer added.

Stacking ModuleAuth alongside UserAuth/AdminAuth is coherent and makes permissions explicit per surface.

Also applies to: 113-113, 143-143, 166-166, 218-226, 241-241


208-216: New /user_group admin routes: clear and scoped.

Good separation and module-guarding for group management CRUD.


186-188: No RBAC changes required for “console.detail”. The key is hard-coded as always allowed in middleware/auth.go (in isUserModuleAllowed and allowedModules) and likewise bypassed in controller checks, so /data/self will not 403.

model/user.go (4)

10-10: LGTM: importing operation_setting for feature gating is appropriate.


49-50: Avatar field addition looks fine; ensure large payloads aren’t returned by default.
Storing base64 in LONGTEXT is acceptable given the 2MB server-side validation. Keep responses trimming avatar except where explicitly fetched (as you already do).


411-411: Call-site update to exported GenerateDefaultSidebarConfigForRole is correct.


471-478: Including avatar in admin Edit updates is correct.

web/src/components/settings/personal/components/UserInfoHeader.jsx (5)

20-33: Imports and dependency wiring look good.


219-256: Upload + Avatar integration is clean; good UX with hoverMask.


343-345: Group label fallback logic is correct and i18n-safe.

Also applies to: 391-393


398-423: Preview flow and confirm upload are well-structured.


90-127: beforeUpload return shape matches Semi UI Upload’s contract. The returned object uses only allowed fields (shouldUpload, status, validateMessage, autoRemove) and aligns with the Semi UI Upload documentation—no changes needed.

controller/user.go (7)

437-446: Invitation gating on TransferAffQuota is correct.


473-481: Invitation gating on GetAffCode is correct.


518-520: GetSelf: avatar cleared and provided via dedicated endpoint — good.

Also applies to: 576-579


617-741: calculateFinalSidebarConfig reads system config safely and respects role guards.


1483-1486: Defaulting group to “default” and preserving it on create are correct.

Also applies to: 1501-1502


1596-1603: Role change syncing of sidebar config is a nice touch.

Also applies to: 1621-1628


1350-1379: Password update gating is safer and avoids unintended validations.

Comment thread router/api-router.go
Comment thread web/src/context/User/reducer.js
Comment thread web/src/helpers/userDataManager.js
Comment thread web/src/hooks/dashboard/useDashboardData.js
Comment thread web/src/i18n/locales/en.json

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

♻️ Duplicate comments (4)
controller/user_group.go (4)

146-151: Ensure UpdateTx whitelists updatable columns (mass-assignment guard).

Confirm model.UserGroup.UpdateTx only updates allowed fields (name, ratio, description, updated_time).

#!/bin/bash
# Inspect UpdateTx to verify it doesn't call Updates on the whole struct.
rg -n -C3 'func\s+\(\s*\*\s*UserGroup\s*\)\s*UpdateTx\('
rg -n -C3 'Updates\(' model | sed -n '1,200p'

Expected: Updates via explicit column whitelist (e.g., Select+Updates(map[string]interface{...})).


3-15: Add missing imports: gorm clause (compile fix) and utf8 (for length checks).

clause.Column is used on Line 172 but gorm.io/gorm/clause isn’t imported, causing a compile error. Also add unicode/utf8 for the name-length validations suggested below.

 import (
 	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
 
 	"one-api/common"
 	"one-api/model"
 	"one-api/setting"
 	"one-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
+	"gorm.io/gorm/clause"
+	"unicode/utf8"
 )

105-116: Mirror name length validation in Update.

Keep Create/Update consistent; avoids rename-induced DB issues.

 	g.Name = strings.TrimSpace(g.Name)
 	if g.Name == "" {
 		common.ApiErrorMsg(c, "分组名称不能为空")
 		return
 	}
+	if utf8.RuneCountInString(g.Name) > 64 {
+		common.ApiErrorMsg(c, "分组名称长度不能超过 64 个字符")
+		return
+	}

44-61: Validate name length (≤64 runes) to match DB schema.

Prevents DB errors/truncation against gorm:"size:64" columns.

 	g.Name = strings.TrimSpace(g.Name)
 	if g.Name == "" {
  		common.ApiErrorMsg(c, "分组名称不能为空")
  		return
  	}
+	if utf8.RuneCountInString(g.Name) > 64 {
+		common.ApiErrorMsg(c, "分组名称长度不能超过 64 个字符")
+		return
+	}
  	// 禁止使用系统保留分组名
  	if isReservedGroup(strings.ToLower(g.Name)) {
  		common.ApiErrorMsg(c, "不能使用系统保留分组名:default、vip、svip")
  		return
  	}
🧹 Nitpick comments (6)
controller/user_group.go (6)

124-132: Reject renaming “into” reserved names before starting the transaction.

Fail fast and avoid unnecessary writes. Remove duplicate in-TX check.

 	// 名称冲突检查
 	if dup, err := model.IsUserGroupNameDuplicated(g.Id, g.Name); err != nil {
 		common.ApiError(c, err)
 		return
 	} else if dup {
 		common.ApiErrorMsg(c, "分组名称已存在")
 		return
 	}
 
+	// 若要改名,先阻止将名称改为系统保留名(TX 之前提前返回)
+	if oldGroup.Name != g.Name && isReservedGroup(g.Name) {
+		common.ApiErrorMsg(c, "不能将分组名称修改为系统保留分组名")
+		return
+	}
@@
-		if isReservedGroup(strings.ToLower(g.Name)) {
-			tx.Rollback()
-			common.ApiErrorMsg(c, "不能将分组名称修改为系统保留分组名")
-			return
-		}

Also applies to: 155-159


133-151: Use GORM Transaction with request context to simplify rollback paths.

Reduces manual rollback/commit code and ties operations to request cancellation.

// Example restructure
err := model.DB.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
    if err := g.UpdateTx(tx); err != nil {
        return err
    }
    if oldGroup.Name != g.Name {
        // reserved-name checks (old already blocked), user updates, etc.
        if err := tx.Model(&model.User{}).
            Where("`group` = ?", oldGroup.Name).
            Update(clause.Column{Name: "group"}, g.Name).Error; err != nil {
            return err
        }
    }
    return nil
})
if err != nil {
    common.ApiError(c, err)
    return
}

Also applies to: 182-187


245-247: Unify terminology: “系统保留分组”.

Keep messages consistent with Create/Update.

-		common.ApiErrorMsg(c, "不能删除系统默认分组")
+		common.ApiErrorMsg(c, "不能删除系统保留分组")

299-306: Trim description before defaulting/using it.

Avoids whitespace-only descriptions and normalizes input.

 func syncGroupToUserUsableGroups(groupName, description string, add bool) error {
 	userUsableGroups := setting.GetUserUsableGroupsCopy()
 
 	if add {
+		description = strings.TrimSpace(description)
 		if description == "" {
 			description = groupName + "分组"
 		}

289-291: Extract option keys to constants to prevent typos and ease reuse.

Small hygiene improvement.

-	if err := model.UpdateOption("GroupRatio", string(jsonBytes)); err != nil {
+	if err := model.UpdateOption(optionGroupRatioKey, string(jsonBytes)); err != nil {
 		return err
 	}
@@
-	if err := model.UpdateOption("UserUsableGroups", string(jsonBytes)); err != nil {
+	if err := model.UpdateOption(optionUserUsableGroupsKey, string(jsonBytes)); err != nil {
 		return err
 	}
@@
-	if err := model.UpdateOption("TopupGroupRatio", string(jsonBytes)); err != nil {
+	if err := model.UpdateOption(optionTopupGroupRatioKey, string(jsonBytes)); err != nil {
 		return err
 	}

Add near the top of this file (or a shared constants file):

const (
	optionGroupRatioKey        = "GroupRatio"
	optionUserUsableGroupsKey  = "UserUsableGroups"
	optionTopupGroupRatioKey   = "TopupGroupRatio"
)

Also applies to: 316-318, 340-342


273-346: Avoid lost updates across concurrent admin writes on JSON-backed settings.

Current read-modify-write on whole maps can drop concurrent changes (last write wins). Consider moving “merge then persist” under a single package-level lock in each settings package, or expose tx-aware helpers that accept a mutator func which operates on the live map under lock before persisting to DB.

📜 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 acd5492 and 5615e9d.

📒 Files selected for processing (2)
  • controller/user_group.go (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/table/users/modals/UserGroupManagement.jsx
🧰 Additional context used
🧬 Code graph analysis (1)
controller/user_group.go (8)
model/user_group.go (5)
  • GetAllUserGroups (51-55)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (78-86)
  • GetUserGroupById (58-65)
  • IsUserGroupInUse (89-93)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-50)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
common/topup-ratio.go (2)
  • GetTopupGroupRatioCopy (16-24)
  • UpdateTopupGroupRatioByJSONString (36-45)
🔇 Additional comments (1)
controller/user_group.go (1)

326-333: Good: thread-safe copy of TopupGroupRatio before mutation.

This eliminates “concurrent map iteration and map write” panics.

@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

♻️ Duplicate comments (4)
controller/user_group.go (1)

147-151: Ensure model.UpdateTx whitelists fields to prevent mass-assignment.

Re-verify that only name/ratio/description (and timestamps) are updated; avoid broad Updates on the full struct.

#!/bin/bash
# Inspect UpdateTx to confirm column whitelist (no mass assignment).
rg -n -C3 'func\s+\(\s*\*\s*UserGroup\s*\)\s*UpdateTx\('
rg -n -C5 'UpdateTx\(' model/user_group.go
rg -n -C2 'Updates\s*\(' model/user_group.go

Expected: Model(&UserGroup{}).Where("id = ?").Select(...).Updates(map[string]any{...})

controller/user.go (3)

852-856: Use typed-bool check; avoid interface equality pitfalls.

Same as prior feedback; assert bool before comparing.

-    systemSectionEnabled, hasEnabled := sectionObj["enabled"]
-    if !hasEnabled || systemSectionEnabled != true {
+    sectionEnabled := true
+    if v, has := sectionObj["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

959-962: Same typed-bool issue here; mirror the explicit bool handling.

Prevents surprises when value isn’t a bool.

-    systemSection, ok := sectionValue.(map[string]interface{})
-    if !ok || systemSection["enabled"] != true {
+    systemSection, ok := sectionValue.(map[string]interface{})
+    if !ok {
+      continue
+    }
+    sectionEnabled := true
+    if v, has := systemSection["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

1099-1134: Respect child toggles in nested modules; merge user prefs with system defaults.

Currently only “enabled” merges; child booleans (e.g., admin.user.groupManagement) ignore user overrides.

-        // 合并系统配置和用户偏好
-        finalNestedObj := make(map[string]interface{})
-        for k, v := range nestedObj {
-          if k == "enabled" {
-            finalNestedObj[k] = effectiveEnabled
-          } else {
-            // 其他字段保持系统配置
-            finalNestedObj[k] = v
-          }
-        }
-
-        // 如果区域被禁用,强制将嵌套对象的enabled设置为false
-        if !userSectionEnabled {
-          finalNestedObj["enabled"] = false
-        }
+        parentEnabled := true
+        if b, ok := effectiveEnabled.(bool); ok {
+          parentEnabled = b
+        }
+        finalNestedObj := make(map[string]interface{})
+        for k, v := range nestedObj {
+          if k == "enabled" {
+            finalNestedObj[k] = parentEnabled && userSectionEnabled
+            continue
+          }
+          switch sv := v.(type) {
+          case bool:
+            userAllow := true
+            if uv, ok := userNestedObj[k]; ok {
+              if ub, ok := uv.(bool); ok {
+                userAllow = ub
+              }
+            }
+            finalNestedObj[k] = sv && userAllow && parentEnabled && userSectionEnabled
+          default:
+            finalNestedObj[k] = v
+          }
+        }
🧹 Nitpick comments (12)
controller/user_group.go (5)

39-46: Ignore client-supplied ID on create.

Prevents accidental/malicious ID injection.

 	if err := c.ShouldBindJSON(&g); err != nil {
 		common.ApiError(c, err)
 		return
 	}
+	// 强制忽略客户端传入的 ID
+	g.Id = 0
 	g.Name = strings.TrimSpace(g.Name)

169-173: Quote reserved column “group” with clause.Column for portability.

Removes ambiguity with reserved keywords across dialects.

-		result := tx.Model(&model.User{}).
-			Where("`group` = ?", oldGroup.Name).
-			Update("group", g.Name)
+		result := tx.Model(&model.User{}).
+			Where("`group` = ?", oldGroup.Name).
+			Update(clause.Column{Name: "group"}, g.Name)

Add import:

 import (
  	"encoding/json"
  	"fmt"
+ 	"gorm.io/gorm/clause"
  	"strconv"
  	"strings"

50-51: Drop redundant strings.ToLower; helper already normalizes.

Simplifies calls and avoids double work.

-	if isReservedGroup(strings.ToLower(g.Name)) {
+	if isReservedGroup(g.Name) {
-		if isReservedGroup(strings.ToLower(g.Name)) {
+		if isReservedGroup(g.Name) {
-		if isReservedGroup(strings.ToLower(oldGroup.Name)) {
+		if isReservedGroup(oldGroup.Name) {

Also applies to: 155-158, 163-166


245-247: Unify wording: use “保留分组” to match checks elsewhere.

Minor copy edit for consistency.

-		common.ApiErrorMsg(c, "不能删除系统默认分组")
+		common.ApiErrorMsg(c, "不能删除系统保留分组")

273-346: Avoid lost updates when syncing JSON-backed settings.

Current read-modify-write with a copied map can drop concurrent changes. Prefer atomic helpers inside setting/common packages that mutate under the same lock and persist immediately.

  • Example API to add (outside this file):
    • ratio_setting.UpsertGroupRatioAndPersist(name string, ratio float64) error
    • ratio_setting.RemoveGroupRatioAndPersist(name string) error
    • setting.UpsertUserUsableGroupAndPersist(name, desc string) error
    • setting.RemoveUserUsableGroupAndPersist(name string) error
    • common.UpsertTopupGroupRatioAndPersist(name string, ratio float64) error
    • common.RemoveTopupGroupRatioAndPersist(name string) error

These would lock, update the in-memory map, serialize once, and call model.UpdateOption atomically per operation.

model/user.go (3)

49-50: Avatar storage in DB (longtext) may bloat; consider URL/object storage or a hard cap.

Base64 inflates size ~33%. Prefer storing in object storage and persisting only URL/etag; if you must keep in DB, enforce a strict limit and purge strategy.


461-469: Validate avatar server-side in the model to cover all write paths.

Admin UpdateUser/Edit can bypass controller.validateAvatar(). Add a lightweight guard in UpdateAvatar (and optionally Edit when avatar present).

Example:

 func (user *User) UpdateAvatar() error {
+  if user.Avatar != "" && !strings.HasPrefix(user.Avatar, "data:image/") {
+    return errors.New("头像数据格式无效")
+  }
   if err := DB.Model(&User{}).Where("id = ?", user.Id).Update("avatar", user.Avatar).Error; err != nil {
     return err
   }
   // Update cache
   DB.First(&user, user.Id)
   return updateUserCache(*user)
 }

Also applies to: 487-488


943-1010: Group rename: surface no-op and bound cache refresh.

Return an error when no rows are affected; helps catch typos. Also consider chunking IDs if groups are large.

   result := tx.Model(&User{}).Where(commonGroupCol+" = ?", oldGroupName).Update(commonGroupCol, newGroupName)
   if result.Error != nil {
     tx.Rollback()
     common.SysLog(fmt.Sprintf("更新用户分组名称失败: %s", result.Error.Error()))
     return result.Error
   }
+  if result.RowsAffected == 0 {
+    tx.Rollback()
+    return gorm.ErrRecordNotFound
+  }
controller/user.go (4)

31-84: Make data URL validation robust; allow standard parameters and tighter match.

Current exact-equals MIME checks reject valid forms (e.g., charset=…). Use a regex for image/* with “;base64,” and keep the 2MB check.

-func validateAvatar(avatarData string) error {
+func validateAvatar(avatarData string) error {
   if avatarData == "" {
     return nil // 允许空头像
   }
-  // 检查是否是有效的base64数据
-  if !strings.HasPrefix(avatarData, "data:image/") {
-    return fmt.Errorf("头像必须是有效的图片格式")
-  }
-  // 提取base64数据部分
-  parts := strings.Split(avatarData, ",")
-  if len(parts) != 2 {
-    return fmt.Errorf("头像数据格式无效")
-  }
-  // 检查MIME类型
-  mimeType := parts[0]
-  allowedTypes := []string{
-    "data:image/jpeg;base64",
-    "data:image/jpg;base64",
-    "data:image/png;base64",
-    "data:image/gif;base64",
-    "data:image/webp;base64",
-  }
-  isValidType := false
-  for _, allowedType := range allowedTypes {
-    if mimeType == allowedType {
-      isValidType = true
-      break
-    }
-  }
-  if !isValidType {
-    return fmt.Errorf("不支持的图片格式,仅支持 JPEG、PNG、GIF、WebP")
-  }
-  // 解码base64数据检查大小
-  base64Data := parts[1]
+  // 允许标准参数:data:image/<type>[;charset=utf-8];base64,<data>
+  re := regexp.MustCompile(`^data:image/(jpeg|jpg|png|gif|webp)(;[^,]*)?;base64,`)
+  if !re.MatchString(avatarData) {
+    return fmt.Errorf("不支持的图片格式,仅支持 JPEG、PNG、GIF、WebP")
+  }
+  parts := strings.SplitN(avatarData, ",", 2)
+  if len(parts) != 2 {
+    return fmt.Errorf("头像数据格式无效")
+  }
+  base64Data := parts[1]
   decodedData, err := base64.StdEncoding.DecodeString(base64Data)
   if err != nil {
     return fmt.Errorf("头像数据解码失败")
   }
   // 检查文件大小(2MB限制)
   const maxSize = 2 * 1024 * 1024 // 2MB
   if len(decodedData) > maxSize {
     return fmt.Errorf("头像文件大小不能超过2MB")
   }
   return nil
 }

Add import:

// at top
import "regexp"

176-177: Avoid returning large avatar blob in Login; keep avatar via dedicated endpoint.

Align with GetSelf; omit avatar from the login payload to reduce payload size.

-        Avatar:      user.Avatar,

575-579: Remove “avatar” key from GetSelf response to truly omit data.

You set user.Avatar = "" but still include "avatar": "" in a map; omit the key to reduce noise.

-        "avatar":            user.Avatar,

1227-1273: Prefer HTTP caching (ETag/If-None-Match) over custom headers.

Replacing X-Avatar-Fetched and ad-hoc session with ETag on avatar hash reduces round-trips and keeps semantics standard.

I can draft an ETag-based handler if helpful.

📜 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 b8e8b4b and d0970ce.

📒 Files selected for processing (4)
  • controller/user.go (16 hunks)
  • controller/user_group.go (1 hunks)
  • model/user.go (10 hunks)
  • web/src/helpers/userDataManager.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/helpers/userDataManager.js
🧰 Additional context used
🧬 Code graph analysis (3)
controller/user.go (6)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
dto/user_settings.go (1)
  • UserSetting (3-13)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleAdminUser (135-135)
model/user.go (3)
  • GetUserById (295-307)
  • User (20-50)
  • GenerateDefaultSidebarConfigForRole (97-162)
common/gin.go (1)
  • ApiError (95-100)
common/sys_log.go (1)
  • SysLog (10-13)
controller/user_group.go (8)
model/user_group.go (5)
  • GetAllUserGroups (51-55)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (78-86)
  • GetUserGroupById (58-65)
  • IsUserGroupInUse (89-93)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-50)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
common/topup-ratio.go (2)
  • GetTopupGroupRatioCopy (16-24)
  • UpdateTopupGroupRatioByJSONString (36-45)
model/user.go (6)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
model/log.go (1)
  • RecordLog (78-94)
logger/logger.go (1)
  • LogQuota (94-100)
model/main.go (1)
  • DB (63-63)
common/sys_log.go (1)
  • SysLog (10-13)
common/redis.go (1)
  • RedisEnabled (17-17)
🔇 Additional comments (8)
controller/user_group.go (1)

133-187: Good transactional flow for rename + user updates.

Atomic rename, proper rollbacks on all early exits, and panic-safe defer look solid.

model/user.go (2)

96-161: Default sidebar config and nested admin.user look good.

Exported generator + nested user.groupManagement enablement is consistent with the new UI/permissions.

Also applies to: 132-150


425-439: Confirm inviter reward semantics (quota vs. invite quota).

You log “赠送” to inviter but no longer increase Quota; only AffQuota increments via inviteUser(). If product expects immediate usable quota, re-enable IncreaseUserQuota; otherwise reword the log to avoid confusion.

Apply one of:

-        RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
+        RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s(已计入邀请额度,可在个人中心划转)", logger.LogQuota(common.QuotaForInviter)))

Or:

         if common.QuotaForInviter > 0 {
+            _ = IncreaseUserQuota(inviterId, common.QuotaForInviter, true)
             RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", logger.LogQuota(common.QuotaForInviter)))
         }
controller/user.go (5)

437-446: Invitation gating LGTM.

Early exit when disabled is clear and user-friendly.


473-482: Aff code gating LGTM.

Consistent with TransferAffQuota handling.


1526-1528: Default group assignment LGTM.

Prevents empty group edge cases on user creation.


1638-1670: Auto-sync sidebar on role change LGTM.

Keeps UI consistent after promote/demote.


618-634: Remove unused userSetting parameter
userSetting isn’t referenced in calculateFinalSidebarConfig; remove it from the signature and update its sole call at controller/user.go:528 to calculateFinalSidebarConfig(userRole).

Comment thread controller/user_group.go
Comment thread controller/user_group.go
Comment thread controller/user_group.go

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

Caution

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

⚠️ Outside diff range comments (2)
controller/user.go (1)

169-177: Don’t return base64 avatar in login response; use the dedicated endpoint.

Large base64 payloads on every login inflate latency and bandwidth, and contradict the intent to fetch avatar via /user/avatar.

   cleanUser := model.User{
     Id:          user.Id,
     Username:    user.Username,
     DisplayName: user.DisplayName,
     Role:        user.Role,
     Status:      user.Status,
     Group:       user.Group,
-    Avatar:      user.Avatar,
   }
controller/user_group.go (1)

1-347: Restrict fields in UpdateTx to avoid mass-assignment
The UpdateTx method in model/user_group.go (lines 40–43) currently calls tx.Model(g).Updates(g), which applies all struct fields and risks overwriting unintended columns. Change this to explicitly whitelist only the fields you want to update (e.g. use Updates(map[string]any{"name": g.Name, "description": g.Description, "ratio": g.Ratio}) or GORM’s Select(...)).

♻️ Duplicate comments (15)
web/src/context/User/reducer.js (1)

29-34: Remove side effects from reducer; run logout cleanup in an effect/action instead.

Reducers must be pure. The dynamic import + async cleanup inside the reducer can execute multiple times under StrictMode and makes state updates non-deterministic. Move this to the caller (e.g., a hook/effect that reacts to logout) or an action creator.

Apply this diff to keep the reducer pure:

-      // 当用户登出时,清理头像缓存和会话标记
-      if (state.user?.id) {
-        import('../../helpers/userDataManager').then(({ cleanupOnLogout }) => {
-          cleanupOnLogout(state.user.id);
-        });
-      }

Example outside the reducer (e.g., in a provider/hook using useReducer):

// pseudo in Provider.jsx
import { cleanupOnLogout } from '../../helpers/userDataManager';
const [state, dispatch] = useReducer(reducer, initialState);
const prevUserRef = useRef();

useEffect(() => {
  const prev = prevUserRef.current;
  if (prev?.id && !state.user) {
    cleanupOnLogout(prev.id);
  }
  prevUserRef.current = state.user;
}, [state.user]);

If you must keep a temporary inline call, at minimum handle failures to avoid unhandled rejections:

-        import('../../helpers/userDataManager').then(({ cleanupOnLogout }) => {
-          cleanupOnLogout(state.user.id);
-        });
+        import('../../helpers/userDataManager')
+          .then(({ cleanupOnLogout }) => cleanupOnLogout?.(state.user.id))
+          .catch((e) => console.error('logout cleanup import failed:', e));

Run to find logout dispatch sites to relocate cleanup:

#!/bin/bash
rg -nP -C2 --type=js --type=jsx --type=ts --type=tsx "\bdispatch\s*\(\s*{[^}]*type\s*:\s*['\"]logout['\"]" 
web/src/components/table/users/modals/UserGroupManagement.jsx (3)

18-18: Remove stray “+” before comment terminator.

Tidy up the license block end.

-+*/
+*/

57-95: DRY: extract permission logic into a shared hook.

This duplicates logic already used in EditUserGroupModal; extract to useGroupManagement() and reuse.

If helpful, I can generate the hook and apply the refactor across both files.


132-137: Fix permission check: null (loading) is treated as denial.

This still shows an error during loading; handle tri-state as elsewhere.

-  const deleteGroup = async (id) => {
-    // 检查权限
-    if (!hasGroupManagementPermission()) {
-      showError(t('无权访问分组管理功能'));
-      return;
-    }
+  const deleteGroup = async (id) => {
+    // 检查权限
+    const perm = hasGroupManagementPermission();
+    if (perm === null) return; // 等待权限加载
+    if (perm === false) {
+      showError(t('无权访问分组管理功能'));
+      return;
+    }
controller/user.go (3)

852-856: Avoid interface-to-bool equality; use typed coercion.

This equality check misbehaves for non-bool types.

-    systemSectionEnabled, hasEnabled := sectionObj["enabled"]
-    if !hasEnabled || systemSectionEnabled != true {
+    sectionEnabled := true
+    if v, has := sectionObj["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

958-962: Same typed-bool issue here.

Mirror the explicit bool handling to avoid interface equality quirks.

-    systemSection, ok := sectionValue.(map[string]interface{})
-    if !ok || systemSection["enabled"] != true {
+    systemSection, ok := sectionValue.(map[string]interface{})
+    if !ok {
+      continue
+    }
+    sectionEnabled := true
+    if v, has := systemSection["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

1099-1128: Nested module user prefs are ignored; merge child toggles.

Child-level booleans (e.g., admin.user.groupManagement) aren’t honoring user overrides.

-        // 计算有效的enabled:支持用户以布尔值直接覆盖嵌套对象(个人设置场景)
+        // 计算有效的enabled:支持用户以布尔值直接覆盖嵌套对象(个人设置场景)
         var effectiveEnabled interface{}
         if userBool, ok := userSection[moduleKey].(bool); ok {
           effectiveEnabled = userBool
         } else if ue, exists := userNestedObj["enabled"]; exists {
           effectiveEnabled = ue
         } else if sysEnabled, has := nestedObj["enabled"]; has {
           effectiveEnabled = sysEnabled
         } else {
           effectiveEnabled = true
         }
-
-        // 合并系统配置和用户偏好
-        finalNestedObj := make(map[string]interface{})
-        for k, v := range nestedObj {
-          if k == "enabled" {
-            finalNestedObj[k] = effectiveEnabled
-          } else {
-            // 其他字段保持系统配置
-            finalNestedObj[k] = v
-          }
-        }
-
-        // 如果区域被禁用,强制将嵌套对象的enabled设置为false
-        if !userSectionEnabled {
-          finalNestedObj["enabled"] = false
-        }
+        parentEnabled := true
+        if b, ok := effectiveEnabled.(bool); ok {
+          parentEnabled = b
+        }
+        // 合并系统配置与用户偏好(包含子功能布尔覆盖)
+        finalNestedObj := make(map[string]interface{})
+        for k, v := range nestedObj {
+          if k == "enabled" {
+            finalNestedObj[k] = parentEnabled && userSectionEnabled
+            continue
+          }
+          switch sv := v.(type) {
+          case bool:
+            userAllow := true
+            if uv, ok := userNestedObj[k]; ok {
+              if ub, ok := uv.(bool); ok {
+                userAllow = ub
+              }
+            }
+            finalNestedObj[k] = sv && userAllow && parentEnabled && userSectionEnabled
+          default:
+            finalNestedObj[k] = v
+          }
+        }
router/api-router.go (1)

176-184: CORS is applied after routes; early handlers won’t get CORS. Move Use() up.

-    logRoute := apiRouter.Group("/log")
-    logRoute.GET("/", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.GetAllLogs)
-    logRoute.DELETE("/", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.DeleteHistoryLogs)
-    logRoute.GET("/stat", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.GetLogsStat)
-    logRoute.GET("/self/stat", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.GetLogsSelfStat)
-    logRoute.GET("/search", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.SearchAllLogs)
-    logRoute.GET("/self", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.GetUserLogs)
-    logRoute.GET("/self/search", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.SearchUserLogs)
-
-    logRoute.Use(middleware.CORS())
+    logRoute := apiRouter.Group("/log")
+    logRoute.Use(middleware.CORS())
+    logRoute.GET("/", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.GetAllLogs)
+    logRoute.DELETE("/", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.DeleteHistoryLogs)
+    logRoute.GET("/stat", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.GetLogsStat)
+    logRoute.GET("/self/stat", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.GetLogsSelfStat)
+    logRoute.GET("/search", middleware.AdminAuth(), middleware.ModuleAuth("console.log"), controller.SearchAllLogs)
+    logRoute.GET("/self", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.GetUserLogs)
+    logRoute.GET("/self/search", middleware.UserAuth(), middleware.ModuleAuth("console.log"), controller.SearchUserLogs)

Also applies to: 189-192

web/src/hooks/common/useSidebar.js (3)

34-66: Use minimal anonymous fallback for 401/403; avoid exposing admin UI on errors

Defaulting to a fully-enabled sidebar on auth/network errors can surface admin modules in UI. Add a minimalAnonymousSidebarConfig and branch on status.

   // 默认配置
   const defaultSidebarConfig = {
@@
     }
   };
+
+  // 未登录/无权限的最小可用配置
+  const minimalAnonymousSidebarConfig = {
+    chat: { enabled: true, playground: true, chat: true },
+    console: { enabled: true, detail: true },
+    personal: { enabled: false },
+    admin: { enabled: false }
+  };
   const loadSidebarConfig = async () => {
     try {
       setLoading(true);
       const res = await API.get('/api/user/self');
       if (res.data.success && res.data.data.sidebar_config) {
         setSidebarConfig(res.data.data.sidebar_config);
       } else {
         // 使用默认配置
         setSidebarConfig(defaultSidebarConfig);
       }
     } catch (error) {
-      // 出错时使用默认配置
-      setSidebarConfig(defaultSidebarConfig);
+      // 区分未登录/无权限与其它错误
+      const status = error?.response?.status;
+      if (status === 401 || status === 403) {
+        setSidebarConfig(minimalAnonymousSidebarConfig);
+      } else {
+        setSidebarConfig(defaultSidebarConfig);
+      }
     } finally {
       setLoading(false);
     }
   };

Also applies to: 67-84


23-29: Make global event target SSR-safe

Accessing window at import time crashes during SSR. Wrap with a getter and no-op fallback.

-// 创建一个全局事件系统来同步所有useSidebar实例
-if (!window.sidebarEventTarget) {
-  window.sidebarEventTarget = new EventTarget();
-}
-const sidebarEventTarget = window.sidebarEventTarget;
+// 创建一个全局事件系统来同步所有useSidebar实例(SSR 安全)
+const getSidebarEventTarget = () => {
+  if (typeof window !== 'undefined') {
+    if (!window.sidebarEventTarget) {
+      window.sidebarEventTarget = new EventTarget();
+    }
+    return window.sidebarEventTarget;
+  }
+  // SSR fallback: no-op event target
+  return { addEventListener() {}, removeEventListener() {}, dispatchEvent() {} };
+};
+const sidebarEventTarget = getSidebarEventTarget();

20-20: Avoid self-triggered double reloads; include source id in events

Current instance reloads twice (direct await + handling its own event). Tag events with instanceId and ignore self.

-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
 export const useSidebar = () => {
-  const [sidebarConfig, setSidebarConfig] = useState(null);
+  const [sidebarConfig, setSidebarConfig] = useState(null);
+  const instanceId = useRef(Symbol('useSidebarInstance'));
   const refreshUserConfig = async () => {
     await loadSidebarConfig();
-    // 触发全局刷新事件,通知所有useSidebar实例更新
-    sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
+    // 触发全局刷新事件,通知所有useSidebar实例更新(忽略自身)
+    sidebarEventTarget.dispatchEvent(
+      new CustomEvent(SIDEBAR_REFRESH_EVENT, { detail: { sourceId: instanceId.current } })
+    );
   };
-  useEffect(() => {
-    const handleRefresh = () => {
-      loadSidebarConfig();
-    };
+  useEffect(() => {
+    const handleRefresh = (e) => {
+      if (e?.detail?.sourceId === instanceId.current) return;
+      loadSidebarConfig();
+    };
     sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
     return () => {
       sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
     };
   }, []);

Also applies to: 30-32, 86-91, 98-109

controller/user_group.go (3)

44-61: Harden create validation: length limits and ratio precision

Match DB schema (name <=64, description <=255, ratio decimal(10,4)), normalize ratio==0 to 1.0 and round to 4dp.

 	g.Name = strings.TrimSpace(g.Name)
 	if g.Name == "" {
  		common.ApiErrorMsg(c, "分组名称不能为空")
  		return
  	}
+	// 名称长度限制
+	if utf8.RuneCountInString(g.Name) > 64 {
+		common.ApiErrorMsg(c, "分组名称长度不能超过 64 个字符")
+		return
+	}
+	// 描述长度限制
+	if utf8.RuneCountInString(g.Description) > 255 {
+		common.ApiErrorMsg(c, "分组描述长度不能超过 255 个字符")
+		return
+	}
  	// 禁止使用系统保留分组名
  	if isReservedGroup(strings.ToLower(g.Name)) {
  		common.ApiErrorMsg(c, "不能使用系统保留分组名:default、vip、svip")
  		return
  	}
 	if g.Ratio < 0 {
 		common.ApiErrorMsg(c, "分组倍率不能小于0")
 		return
 	}
 	if g.Ratio == 0 {
 		g.Ratio = 1.0 // 默认倍率为1.0
 	}
+	// 统一倍率小数精度至 4 位
+	g.Ratio = math.Round(g.Ratio*1e4) / 1e4

Add imports if missing:

-	"fmt"
+	"fmt"
+	"math"
+	"unicode/utf8"

105-116: Mirror validation on update

Apply same length and precision checks as create.

 	g.Name = strings.TrimSpace(g.Name)
 	if g.Name == "" {
 		common.ApiErrorMsg(c, "分组名称不能为空")
 		return
 	}
+	if utf8.RuneCountInString(g.Name) > 64 {
+		common.ApiErrorMsg(c, "分组名称长度不能超过 64 个字符")
+		return
+	}
+	if utf8.RuneCountInString(g.Description) > 255 {
+		common.ApiErrorMsg(c, "分组描述长度不能超过 255 个字符")
+		return
+	}
 	if g.Ratio < 0 {
 		common.ApiErrorMsg(c, "分组倍率不能小于0")
 		return
 	}
 	if g.Ratio == 0 {
 		g.Ratio = 1.0
 	}
+	g.Ratio = math.Round(g.Ratio*1e4) / 1e4

169-172: Quote reserved column “group” in UPDATE

Use clause.Column to avoid MySQL reserved-word issues.

-		result := tx.Model(&model.User{}).
-			Where("`group` = ?", oldGroup.Name).
-			Update("group", g.Name)
+		result := tx.Model(&model.User{}).
+			Where("`group` = ?", oldGroup.Name).
+			Update(clause.Column{Name: "group"}, g.Name)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)

407-413: Accept both boolean and object-enabled sections

Sections set to true in adminConfig currently get dropped.

   ].filter(section => {
     // 仅显示系统启用的区域
     const sec = adminConfig?.[section.key];
-    const systemAllowed = !!sec && sec.enabled === true;
+    const systemAllowed = typeof sec === 'boolean' ? sec === true : sec?.enabled === true;
     console.log(`区域 ${section.key} 系统是否允许:`, systemAllowed, 'adminConfig:', adminConfig);
     return systemAllowed;
   }).map(section => ({
🧹 Nitpick comments (14)
web/src/hooks/common/useUserPermissions.js (2)

63-67: Default-allow risks unauthorized UI flash; consider default-deny while loading and honor “enabled”.

Right now, when permissions are absent you allow sections/modules by default. That can briefly expose UI until the server answer arrives, and you ignore a possible enabled: false flag on sections.

Apply:

-  const isSidebarSectionAllowed = (sectionKey) => {
-    if (!permissions?.sidebar_modules) return true;
-    const sectionPerms = permissions.sidebar_modules[sectionKey];
-    return sectionPerms !== false;
-  };
+  const isSidebarSectionAllowed = (sectionKey) => {
+    if (loading || !permissions?.sidebar_modules) return false;
+    const sectionPerms = permissions.sidebar_modules[sectionKey];
+    if (sectionPerms === false) return false;
+    if (sectionPerms && typeof sectionPerms === 'object' && sectionPerms.enabled === false) return false;
+    return true;
+  };

-  const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
-    if (!permissions?.sidebar_modules) return true;
-    const sectionPerms = permissions.sidebar_modules[sectionKey];
-    // 如果整个区域被禁用
-    if (sectionPerms === false) return false;
-    // 如果区域存在但模块被禁用
-    if (sectionPerms && sectionPerms[moduleKey] === false) return false;
-    return true;
-  };
+  const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
+    if (loading || !permissions?.sidebar_modules) return false;
+    const sectionPerms = permissions.sidebar_modules[sectionKey];
+    if (sectionPerms === false) return false;
+    if (sectionPerms && typeof sectionPerms === 'object') {
+      if (sectionPerms.enabled === false) return false;
+      if (sectionPerms[moduleKey] === false) return false;
+    }
+    return true;
+  };

Confirm the backend shape of permissions.sidebar_modules includes an enabled flag for sections. If not, keep only the loading default-deny change.

Also applies to: 70-81, 84-104


32-51: Avoid setState after unmount during async load.

Add a mounted guard to prevent React warnings when the component unmounts mid-request.

-  const loadPermissions = async () => {
+  const loadPermissions = async () => {
     try {
       setLoading(true);
       setError(null);
       const res = await API.get('/api/user/self');
       if (res.data.success) {
         const userPermissions = res.data.data.permissions;
-        setPermissions(userPermissions);
+        if (!cancelled) setPermissions(userPermissions);
       } else {
-        setError(res.data.message || '获取权限失败');
+        if (!cancelled) setError(res.data.message || '获取权限失败');
       }
     } catch (error) {
-      setError('网络错误,请重试');
+      if (!cancelled) setError('网络错误,请重试');
     } finally {
-      setLoading(false);
+      if (!cancelled) setLoading(false);
     }
   };
 
   useEffect(() => {
-    loadPermissions();
-  }, []);
+    let cancelled = false;
+    loadPermissions();
+    return () => {
+      cancelled = true;
+    };
+  }, []);

Also applies to: 53-55

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

152-166: Replace min-h-full with min-h-screen on the root container
No ancestor in web/src/App.jsx or web/src/components/layout provides h-full, so min-h-full won’t guarantee full-viewport height. Update the <div> on line 152 of web/src/pages/Home/index.jsx to use min-h-screen.

web/src/components/table/users/modals/UserGroupManagement.jsx (2)

109-129: 403 detection via res.status is ineffective for logical failures.

For non-HTTP failures (success === false with 200), res.status won’t be 403. Handle permission errors in catch (already done) and drop this branch, or check an API error code field if provided.

-      } else {
-        showError(res.data.message || t('获取分组列表失败'));
-        // 如果是权限错误,关闭模态框
-        if (res.status === 403) {
-          onClose();
-        }
-      }
+      } else {
+        showError(res.data.message || t('获取分组列表失败'));
+      }

329-337: Disable actions while permission indeterminate/denied to reduce UX friction.

Prevent opening the modal or showing confirm errors when perm === null/false.

-                <Button
+                <Button
                   type='primary'
                   theme='solid'
                   size='small'
                   icon={<IconPlus />}
-                  onClick={() => handleEdit()}
+                  disabled={hasGroupManagementPermission() !== true}
+                  onClick={() => handleEdit()}
                 >
                   {t('新建分组')}
                 </Button>

And optionally:

-          <Button size='small' onClick={() => handleEdit(record)}>
+          <Button size='small' disabled={hasGroupManagementPermission() !== true} onClick={() => handleEdit(record)}>
             {t('编辑')}
           </Button>

Also applies to: 270-287

controller/user.go (3)

518-520: Omit avatar from GetSelf payload entirely.

You already fetch avatar via a dedicated endpoint; leaving an empty string here is redundant.

-  // 完全移除头像数据,头像通过专用端点获取
-  user.Avatar = ""
...
-    "avatar":            user.Avatar,

Also applies to: 574-579


1039-1040: Typed-bool check for section enabled.

Keep the pattern consistent here too.

-    if !ok || systemSection["enabled"] != true {
+    if !ok {
+      continue
+    }
+    sectionEnabled := true
+    if v, has := systemSection["enabled"]; has {
+      if b, ok := v.(bool); ok {
+        sectionEnabled = b
+      }
+    }
+    if !sectionEnabled {
       continue
     }

1227-1273: Prefer standard HTTP caching (ETag/If-None-Match) over custom headers.

X-Avatar-Fetched and X-Session-ID add client complexity. ETag with 304 Not Modified keeps JSON slim and interoperable.

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

2002-2004: Polish navbar strings for clarity.

-  "导航栏管理": "Navigation bar management",
-  "控制导航栏模块显示状态,全局生效": "Control header module display status, global effect",
+  "导航栏管理": "Navigation Bar Management",
+  "控制导航栏模块显示状态,全局生效": "Control navigation bar module visibility (applies globally)",

2083-2086: Improve invitation toggle description.

-  "关闭后:不再启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled",
+  "关闭后:不再启用邀请奖励功能": "When disabled, invitation rewards are turned off",
controller/user_group.go (2)

3-15: Add clause import for safe reserved-word updates

Upcoming fix uses gorm.io/gorm/clause.

 import (
 	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
 
 	"one-api/common"
 	"one-api/model"
 	"one-api/setting"
 	"one-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
+	"gorm.io/gorm/clause"
 )

124-132: Pre-check new reserved name before starting transaction

Fail fast if g.Name is reserved to avoid entering tx/update at all.

 	// 名称冲突检查
 	if dup, err := model.IsUserGroupNameDuplicated(g.Id, g.Name); err != nil {
 		common.ApiError(c, err)
 		return
 	} else if dup {
 		common.ApiErrorMsg(c, "分组名称已存在")
 		return
 	}
 
+	// 禁止将名称修改为系统保留分组名(预检)
+	if isReservedGroup(strings.ToLower(g.Name)) {
+		common.ApiErrorMsg(c, "不能将分组名称修改为系统保留分组名")
+		return
+	}
+
 	// 使用事务确保分组更新和用户更新的数据一致性
 	tx := model.DB.Begin()
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)

414-421: Remove debug logs for production noise

Drop verbose console logs in handlers/filters.

-      console.log(
-        '用户边栏功能配置变更:',
-        sectionKey,
-        moduleKey,
-        checked,
-        newModules,
-      );
+      // no-op
-    console.log('用户边栏配置重置为默认:', defaultConfig);
+    // no-op
-      console.log('保存用户边栏配置:', sidebarModulesUser);
+      // no-op
-        console.log('用户边栏配置保存成功');
+        // no-op
-        console.log('用户边栏配置已刷新,边栏将立即更新');
+        // no-op
-        console.error('加载边栏配置失败:', error);
+        // console.error('加载边栏配置失败:', error);

Also applies to: 423-426, 228-236, 241-244, 250-265, 331-335


274-337: Parse setting JSON once to reduce overhead

You parse userRes.data.data.setting twice. Parse once and reuse.

If helpful, I can submit a small refactor PR to consolidate these branches.

📜 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 d0970ce and 66dd6f3.

📒 Files selected for processing (53)
  • common/topup-ratio.go (3 hunks)
  • controller/misc.go (4 hunks)
  • controller/pricing.go (3 hunks)
  • controller/user.go (16 hunks)
  • controller/user_group.go (1 hunks)
  • middleware/auth.go (2 hunks)
  • model/main.go (3 hunks)
  • model/option.go (1 hunks)
  • model/user.go (10 hunks)
  • model/user_group.go (1 hunks)
  • router/api-router.go (8 hunks)
  • setting/operation_setting/general_setting.go (1 hunks)
  • web/index.html (1 hunks)
  • web/src/App.jsx (6 hunks)
  • web/src/components/auth/AuthPageLayout.jsx (1 hunks)
  • web/src/components/auth/LoginForm.jsx (3 hunks)
  • web/src/components/auth/ModuleRoute.jsx (1 hunks)
  • web/src/components/auth/PasswordResetConfirm.jsx (3 hunks)
  • web/src/components/auth/PasswordResetForm.jsx (3 hunks)
  • web/src/components/auth/RegisterForm.jsx (3 hunks)
  • web/src/components/layout/Footer.jsx (1 hunks)
  • web/src/components/layout/PageLayout.jsx (1 hunks)
  • web/src/components/layout/headerbar/LanguageSelector.jsx (1 hunks)
  • web/src/components/layout/headerbar/UserArea.jsx (1 hunks)
  • web/src/components/settings/OtherSetting.jsx (1 hunks)
  • web/src/components/settings/PersonalSetting.jsx (4 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (6 hunks)
  • web/src/components/settings/personal/components/UserInfoHeader.jsx (5 hunks)
  • web/src/components/table/users/UsersActions.jsx (1 hunks)
  • web/src/components/table/users/index.jsx (1 hunks)
  • web/src/components/table/users/modals/AddUserModal.jsx (4 hunks)
  • web/src/components/table/users/modals/EditUserGroupModal.jsx (1 hunks)
  • web/src/components/table/users/modals/UserGroupManagement.jsx (1 hunks)
  • web/src/components/topup/index.jsx (6 hunks)
  • web/src/constants/user.constants.js (1 hunks)
  • web/src/context/User/reducer.js (1 hunks)
  • web/src/helpers/api.js (2 hunks)
  • web/src/helpers/avatarCache.js (1 hunks)
  • web/src/helpers/data.js (1 hunks)
  • web/src/helpers/render.jsx (2 hunks)
  • web/src/helpers/userDataManager.js (1 hunks)
  • web/src/helpers/utils.jsx (1 hunks)
  • web/src/hooks/common/useHeaderBar.js (1 hunks)
  • web/src/hooks/common/useSidebar.js (3 hunks)
  • web/src/hooks/common/useUserPermissions.js (1 hunks)
  • web/src/hooks/dashboard/useDashboardData.js (1 hunks)
  • web/src/hooks/model-pricing/useModelPricingData.jsx (2 hunks)
  • web/src/i18n/locales/en.json (4 hunks)
  • web/src/pages/Home/index.jsx (2 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (3 hunks)
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (2 hunks)
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (8 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • model/option.go
🚧 Files skipped from review as they are similar to previous changes (42)
  • web/src/components/layout/Footer.jsx
  • setting/operation_setting/general_setting.go
  • web/src/helpers/avatarCache.js
  • web/index.html
  • web/src/constants/user.constants.js
  • web/src/helpers/data.js
  • web/src/components/layout/headerbar/LanguageSelector.jsx
  • web/src/components/auth/RegisterForm.jsx
  • controller/misc.go
  • web/src/components/table/users/modals/AddUserModal.jsx
  • web/src/components/layout/PageLayout.jsx
  • web/src/components/table/users/modals/EditUserGroupModal.jsx
  • web/src/components/settings/OtherSetting.jsx
  • web/src/components/auth/LoginForm.jsx
  • web/src/helpers/utils.jsx
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx
  • web/src/components/auth/AuthPageLayout.jsx
  • model/user.go
  • web/src/hooks/common/useHeaderBar.js
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx
  • web/src/components/auth/PasswordResetConfirm.jsx
  • web/src/components/auth/ModuleRoute.jsx
  • common/topup-ratio.go
  • web/src/components/table/users/index.jsx
  • web/src/helpers/api.js
  • middleware/auth.go
  • web/src/components/layout/headerbar/UserArea.jsx
  • controller/pricing.go
  • web/src/components/settings/personal/cards/NotificationSettings.jsx
  • web/src/components/settings/PersonalSetting.jsx
  • web/src/helpers/render.jsx
  • web/src/components/settings/personal/components/UserInfoHeader.jsx
  • web/src/components/table/users/UsersActions.jsx
  • model/user_group.go
  • web/src/hooks/dashboard/useDashboardData.js
  • web/src/App.jsx
  • web/src/components/topup/index.jsx
  • web/src/helpers/userDataManager.js
  • web/src/hooks/model-pricing/useModelPricingData.jsx
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
  • model/main.go
  • web/src/components/auth/PasswordResetForm.jsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-02T16:17:53.708Z
Learnt from: x-Ai
PR: QuantumNous/new-api#1703
File: middleware/auth.go:264-293
Timestamp: 2025-09-02T16:17:53.708Z
Learning: The sidebar management system introduced in this codebase uses SidebarModulesAdmin configuration to control admin user permissions. Admin access to console.* modules should be governed by this configuration system, not bypassed with hardcoded allowlists. The system is designed for granular permission control where system administrators can configure which features admin users can access.

Applied to files:

  • web/src/hooks/common/useSidebar.js
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
🧬 Code graph analysis (8)
web/src/components/table/users/modals/UserGroupManagement.jsx (2)
web/src/components/table/users/modals/EditUserGroupModal.jsx (9)
  • useTranslation (41-41)
  • loading (43-43)
  • sidebarLoading (44-44)
  • getUserRole (47-50)
  • isRoot (52-52)
  • isAdmin (53-53)
  • hasGroupManagementPermission (56-83)
  • getGroupDisplayName (93-98)
  • EditUserGroupModal (40-275)
web/src/hooks/common/useSidebar.js (4)
  • loading (32-32)
  • useSidebar (30-180)
  • useSidebar (30-180)
  • finalConfig (112-112)
web/src/hooks/common/useSidebar.js (2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
  • useSidebar (55-55)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • useSidebar (100-100)
controller/user_group.go (8)
model/user_group.go (5)
  • GetAllUserGroups (51-55)
  • UserGroup (8-16)
  • IsUserGroupNameDuplicated (78-86)
  • GetUserGroupById (58-65)
  • IsUserGroupInUse (89-93)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
common/sys_log.go (1)
  • SysLog (10-13)
model/main.go (1)
  • DB (63-63)
model/user.go (1)
  • User (20-50)
setting/ratio_setting/group_ratio.go (2)
  • GetGroupRatioCopy (26-35)
  • UpdateGroupRatioByJSONString (56-62)
setting/user_usable_group.go (2)
  • GetUserUsableGroupsCopy (15-24)
  • UpdateUserUsableGroupsByJSONString (37-43)
common/topup-ratio.go (2)
  • GetTopupGroupRatioCopy (16-24)
  • UpdateTopupGroupRatioByJSONString (36-45)
web/src/context/User/reducer.js (1)
web/src/helpers/userDataManager.js (2)
  • cleanupOnLogout (104-114)
  • cleanupOnLogout (104-114)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)
web/src/components/settings/personal/cards/NotificationSettings.jsx (5)
  • sidebarModulesUser (60-87)
  • adminConfig (88-88)
  • permissionsLoading (91-97)
  • getDefaultSystemConfig (174-203)
  • handleModuleChange (116-127)
web/src/hooks/common/useUserPermissions.js (1)
  • hasSidebarSettingsPermission (58-60)
router/api-router.go (8)
controller/user.go (1)
  • GetUserAvatar (1227-1273)
middleware/auth.go (3)
  • ModuleAuth (180-223)
  • AdminAuth (163-167)
  • UserAuth (157-161)
controller/log.go (3)
  • DeleteHistoryLogs (148-168)
  • GetLogsStat (101-122)
  • GetLogsSelfStat (124-146)
controller/usedata.go (1)
  • GetUserQuotaDates (29-52)
controller/user_group.go (4)
  • GetAllUserGroups (18-25)
  • CreateUserGroup (38-92)
  • UpdateUserGroup (95-219)
  • DeleteUserGroup (222-271)
model/user_group.go (1)
  • GetAllUserGroups (51-55)
controller/midjourney.go (2)
  • GetUserMidjourney (271-294)
  • GetAllMidjourney (246-269)
controller/task.go (2)
  • GetUserTask (252-274)
  • GetAllTask (229-250)
controller/user.go (6)
setting/operation_setting/general_setting.go (1)
  • GetGeneralSetting (25-27)
dto/user_settings.go (1)
  • UserSetting (3-13)
common/constants.go (3)
  • OptionMapRWMutex (37-37)
  • OptionMap (36-36)
  • RoleAdminUser (135-135)
model/user.go (3)
  • GetUserById (295-307)
  • User (20-50)
  • GenerateDefaultSidebarConfigForRole (97-162)
common/gin.go (1)
  • ApiError (95-100)
common/sys_log.go (1)
  • SysLog (10-13)
web/src/pages/Home/index.jsx (1)
web/src/components/layout/NoticeModal.jsx (1)
  • NoticeModal (39-253)
🪛 Biome (2.1.2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

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

Hooks should not be called after an early return.

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)

🔇 Additional comments (12)
web/src/context/User/reducer.js (1)

23-23: Comment-only change looks good.

The inline note clarifies avatar caching happens outside the reducer. No action needed.

web/src/hooks/common/useUserPermissions.js (1)

40-40: Silencing console noise is good.

Commenting out these logs reduces production noise without altering behavior.

Also applies to: 43-43, 47-47

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

191-219: Ignore this suggestion; the Input component’s prop is readonly (lowercase)
Semi UI’s InputProps interface declares a readonly?: boolean prop, so replacing it with readOnly would be incorrect.

Likely an incorrect or invalid review comment.

controller/user.go (3)

437-445: Good: Invitation feature gating added.

Early exit with clear message when InvitationEnabled is false is correct.

Also applies to: 474-482


1321-1360: Good: Avatar-only update path with validation and minimal update.

The narrow-scope write via UpdateAvatar and pre-validation is tidy.


1392-1400: Good: Avatar validation on full UpdateSelf path.

Prevents invalid data sneaking in via the general update flow.

router/api-router.go (2)

58-58: Good: Dedicated avatar endpoint added under user auth.


208-216: Good: New user_group CRUD routes gated by AdminAuth + ModuleAuth.

web/src/hooks/common/useSidebar.js (1)

111-129: Visibility helpers: behavior LGTM

Boolean and nested object.enabled are correctly handled.

controller/user_group.go (2)

147-151: Transaction handling LGTM

Rollback paths and commit error handling are correct.

Also applies to: 182-187


273-346: Sync helpers: thread-safe copies LGTM

Copy/update flows respect in-memory caches and DB options.

web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)

39-47: Hooks order fix LGTM

State/hooks are now declared before early returns.

Comment thread web/src/i18n/locales/en.json
Comment thread web/src/i18n/locales/en.json
Comment thread web/src/pages/Home/index.jsx
Comment thread web/src/pages/Home/index.jsx

@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)
web/src/i18n/locales/en.json (1)

2054-2054: Resolved: improved sidebar phrasing adopted.
Matches prior suggestion; reads naturally now.

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

2001-2003: Polish wording and casing for nav bar texts.

  • "header module" ≠ "navigation bar". Suggest clearer, consistent phrasing and Title Case.

Apply this diff:

-  "导航栏管理": "Navigation bar management",
-  "控制导航栏模块显示状态,全局生效": "Control header module display status, global effect",
-  "系统主页": "System homepage",
+  "导航栏管理": "Navigation Bar Management",
+  "控制导航栏模块显示状态,全局生效": "Control which modules appear in the navigation bar (applies globally)",
+  "系统主页": "System Homepage",

2083-2086: Tighten invitation toggle description.
Current sentence is clunky; simplify.

-  "邀请功能": "Invitation Feature",
-  "关闭后:不再启用邀请奖励功能": "When disabled: invitation reward feature will not be enabled",
+  "邀请功能": "Invitations",
+  "关闭后:不再启用邀请奖励功能": "When disabled: invitation rewards are turned off",

2099-2109: Micro copy improvements in Group Management.

  • Use the multiplication sign × for “倍”.
  • Use standard confirmation phrasing and clearer warning.
-  "倍": "x",
+  "倍": "×",
-  "确定删除此分组?": "Are you sure to delete this group?",
-  "删除后无法恢复,请确认该分组未被用户使用": "Cannot be recovered after deletion, please confirm that this group is not being used by users",
+  "确定删除此分组?": "Are you sure you want to delete this group?",
+  "删除后无法恢复,请确认该分组未被用户使用": "This action cannot be undone. Please ensure no users are assigned to this group.",
📜 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 66dd6f3 and d6250c2.

📒 Files selected for processing (1)
  • web/src/i18n/locales/en.json (4 hunks)
🔇 Additional comments (2)
web/src/i18n/locales/en.json (2)

387-387: LGTM: added role "Guest" is consistent with existing role labels.


2001-2003: i18n: no stale keys and all new keys referenced
Verified that the old key “顶栏管理” has zero usages and the new keys “导航栏管理”, “分组管理”, “用户分组管理” and “邀请功能” are each referenced in the codebase.

Comment thread web/src/i18n/locales/en.json

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

🧹 Nitpick comments (2)
web/src/i18n/locales/en.json (2)

2001-2003: Fix terminology and wording consistency for navigation bar settings.
Use “navigation bar” (not “header”), and tighten copy.

Apply:

-  "控制导航栏模块显示状态,全局生效": "Control header module display status, global effect",
+  "控制导航栏模块显示状态,全局生效": "Control navigation bar module visibility (applies globally)",
-  "系统主页": "System homepage",
+  "系统主页": "System Homepage",

2106-2107: Polish deletion confirmations for clarity.

-  "确定删除此分组?": "Are you sure to delete this group?",
-  "删除后无法恢复,请确认该分组未被用户使用": "Cannot be recovered after deletion, please confirm that this group is not being used by users",
+  "确定删除此分组?": "Are you sure you want to delete this group?",
+  "删除后无法恢复,请确认该分组未被用户使用": "This action cannot be undone. Please confirm no users are assigned to this group.",
📜 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 d6250c2 and 8702d3f.

📒 Files selected for processing (1)
  • web/src/i18n/locales/en.json (4 hunks)
🔇 Additional comments (4)
web/src/i18n/locales/en.json (4)

387-387: Add ‘Guest’ role — looks good.
Translation is accurate and consistent with other role labels.


2054-2054: Phrasing improvement confirmed.
“You can customize which features are shown in the sidebar” reads well.


2081-2082: Yes/No entries — OK.
Consistent with existing capitalization elsewhere.


2093-2095: Verify frontend validation matches backend constraints for group name/ratio.
Ensure the regex (letters, numbers, underscores, hyphens) and “ratio ≥ 0” align with server-side validation and error messages to avoid mismatch.

Would you like a follow-up PR to wire these rules to a shared validation schema?

Also applies to: 2100-2102

Comment thread web/src/i18n/locales/en.json
Comment thread web/src/i18n/locales/en.json Outdated

@creamlike1024 creamlike1024 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

感谢贡献

有两个小建议,可以参考一下:

  1. 多个功能建议拆分成多个 PR(比如一个 PR 专注一个 feature/fix),这样每个改动可以单独讨论、测试和快速合并 ✅
  2. 在推送前,可以用 git rebase -i 整理一下 commit 历史,让提交信息更清晰、连贯 🧹

@creamlike1024
creamlike1024 self-requested a review September 11, 2025 14:01

@creamlike1024 creamlike1024 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

请解决分组管理的数据库问题

Comment thread model/user_group.go
Comment thread model/user_group.go Outdated
@creamlike1024

Copy link
Copy Markdown
Collaborator

如果不好修改可以先将分组管理以外的功能单独提 PR 进行合并

1.新增"邀请功能"开关
2.i18n本地化
3.修复侧边栏权限同步问题
1.新增分组管理功能
2.修复登录页面样式错位,统一主页和登录页面风格
1.潜在的权限问题
2.用户体验等问题
@x-Ai
x-Ai requested a review from creamlike1024 September 18, 2025 15:28
@Calcium-Ion

Copy link
Copy Markdown
Member

已经说过了,这个pr改的东西太多,代码提交修改太多,只能拆分开提交新的pr,这个pr是不可能再被接收的了

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