Skip to content

新增"顶栏"、"侧边栏"管理功能 - #1701

Merged
creamlike1024 merged 1 commit into
QuantumNous:alphafrom
x-Ai:alpha
Aug 31, 2025
Merged

新增"顶栏"、"侧边栏"管理功能#1701
creamlike1024 merged 1 commit into
QuantumNous:alphafrom
x-Ai:alpha

Conversation

@x-Ai

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

Copy link
Copy Markdown
Contributor

新增设置 "顶栏"、"侧边栏"管理功能,提高用户体验

  1. 完整的功能鉴权
  2. 全面i18n
个人设置 系统设置

Summary by CodeRabbit

  • New Features
    • Dynamic header navigation: admin-configurable modules and optional auth gate for Pricing.
    • Role- and config-aware sidebar: admin controls sections/modules; users personalize within limits, persisted to profile.
    • UI now derives permissions and sidebar settings from user data for tailored visibility.
  • UI
    • New settings panels: Header Navigation (operations) and Sidebar Modules (admin/user).
    • Pricing link auto-redirects to login when authentication is required.
  • Localization
    • Expanded English translations for new settings and sidebar/navigation labels.

增加用户体验
@coderabbitai

coderabbitai Bot commented Aug 30, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds configurable header and sidebar modules across backend and frontend. Backend now returns HeaderNavModules and SidebarModulesAdmin in status, manages user roles, permissions, and per-user sidebar_modules with defaults and updates. Frontend consumes these configs to gate navigation (pricing auth), render dynamic sidebar visibility, and provide admin/user UIs to edit and persist configurations.

Changes

Cohort / File(s) Summary
Status payload additions
controller/misc.go
GetStatus now includes HeaderNavModules and SidebarModulesAdmin from options.
User role, permissions, and settings
controller/user.go, dto/user_settings.go, model/user.go
Default role set on register; CreateUser preserves role. GetSelf now returns composed data with permissions and sidebar_modules. UpdateSelf supports updating sidebar_modules. DTO adds SidebarModules. Model initializes role-based default SidebarModules post-insert.
Header navigation config and auth gating
web/src/App.jsx, web/src/components/layout/HeaderBar/index.jsx, web/src/components/layout/HeaderBar/Navigation.jsx, web/src/hooks/common/useHeaderBar.js, web/src/hooks/common/useNavigation.js
Introduces headerNavModules parsing and pricingRequireAuth. /pricing route conditionally requires auth. Navigation generation becomes module-driven; Navigation component accepts pricingRequireAuth and redirects pricing to /login when required and unauthenticated.
Sidebar visibility system
web/src/components/layout/SiderBar.jsx, web/src/hooks/common/useSidebar.js, web/src/hooks/common/useUserPermissions.js
Adds hooks to merge admin and user configs into effective visibility. Sidebar sections/items render conditionally via helpers; permission utilities exposed.
Admin settings UI for modules
web/src/components/settings/OperationSetting.jsx, web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx, web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
Adds admin UIs to configure header nav modules and admin sidebar modules. Persists via /api/option and updates StatusContext.
User settings UI for sidebar
web/src/components/settings/personal/cards/NotificationSettings.jsx, web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
Adds user-facing sidebar configuration tab with permission checks, load/save/reset via /api/user/self, filtered by admin config.
Localization
web/src/i18n/locales/en.json
Adds translations for header/sidebar management and related module labels.
Whitespace-only
web/src/components/settings/PersonalSetting.jsx
Formatting-only blank lines; no logic change.

Sequence Diagram(s)

sequenceDiagram
  participant Server as Server (GetStatus)
  participant App as App/useHeaderBar
  participant Nav as Header Navigation
  participant Router as Router
  Server-->>App: status.HeaderNavModules (JSON)
  App->>App: parse modules, derive pricingRequireAuth
  App-->>Nav: pricingRequireAuth
  Router->>App: Route /pricing
  App->>Router: Use PrivateRoute if pricingRequireAuth else public Pricing
  Nav->>Nav: If unauth && pricingRequireAuth => link to /login
Loading
sequenceDiagram
  participant Server as Server
  participant Sidebar as useSidebar
  participant UI as SiderBar.jsx
  Server-->>Sidebar: Status.SidebarModulesAdmin (JSON)
  Sidebar->>Server: GET /api/user/self
  Server-->>Sidebar: sidebar_modules (string|object)
  Sidebar->>Sidebar: merge adminConfig + userConfig => finalConfig
  Sidebar-->>UI: isModuleVisible(), hasSectionVisibleModules()
  UI->>UI: Conditionally render sections and items
Loading
sequenceDiagram
  participant Client as Settings (User)
  participant API as /api/user/self
  participant Model as model.User
  Client->>API: PUT { sidebar_modules: JSON-string }
  API->>Model: Persist user.Setting.SidebarModules
  Model-->>API: OK
  API-->>Client: Success response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

A nibble of nav, a sidebar hop,
I toggle the switches—flip, flip, pop!
Pricing guards the burrow’s door,
Admin seeds what modules soar.
Users weave their warren’s view,
JSON trails and carrots, too.
Ship it—thump! The UI grew. 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (28)
web/src/hooks/common/useUserPermissions.js (1)

22-23: Clean up dev logs and localize error.

Remove console.log in production; return localized error from consumers instead of hard-coded zh message in the hook.

-        console.log('用户权限加载成功:', userPermissions);
+        // no-op
...
-      setError('网络错误,请重试');
+      setError('NETWORK_ERROR'); // let UI map to i18n

Also applies to: 28-29

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

34-61: DRY: extract DEFAULT_SIDEBAR_MODULES to avoid triple duplication.

Same default object appears in initial state, reset, and fallback. Centralize to prevent drift.

+const DEFAULT_SIDEBAR_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: true, 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: true,
-      setting: true
-    }
-  });
+  const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState(DEFAULT_SIDEBAR_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: true,
-        setting: true
-      }
-    };
-    setSidebarModulesAdmin(defaultModules);
+  function resetSidebarModules() {
+    setSidebarModulesAdmin(DEFAULT_SIDEBAR_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: true, setting: true }
-        };
-        setSidebarModulesAdmin(defaultModules);
+        setSidebarModulesAdmin(DEFAULT_SIDEBAR_MODULES);

Also applies to: 92-123, 168-173

dto/user_settings.go (1)

11-11: Avoid stringly-typed JSON for SidebarModules; validate or use RawMessage

Storing JSON as string is brittle. At minimum, validate it server-side before persisting. Optionally, switch the field to json.RawMessage to avoid double-encoding and enable schema validation later.

If you choose RawMessage, update the type as below and adjust assignments accordingly:

- SidebarModules        string  `json:"sidebar_modules,omitempty"`                // SidebarModules 左侧边栏模块配置
+ SidebarModules        json.RawMessage `json:"sidebar_modules,omitempty"`        // SidebarModules 左侧边栏模块配置

Add import in this file:

import "encoding/json"

Would you like me to add a small validator that rejects malformed JSON for this field in the update path?

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

119-126: UI wiring looks good; consider consistent Card wrappers (nit)
Wrap these sections in Card like others for uniform look-and-feel.

model/user.go (2)

94-154: Default config generator is fine; consider constants and tighter typing (optional)

  • Define module/section keys as constants to avoid drift with frontend.
  • Optionally return json.RawMessage and let callers assign directly without string conversions.

Example constants (outside this hunk):

const (
  sectionChat = "chat"
  sectionConsole = "console"
  sectionPersonal = "personal"
  sectionAdmin = "admin"

  modPlayground = "playground"
  modChat = "chat"
  modDetail = "detail"
  modToken = "token"
  modLog = "log"
  modMidjourney = "midjourney"
  modTask = "task"
  modTopup = "topup"
  modPersonal = "personal"
  modChannel = "channel"
  modModels = "models"
  modRedemption = "redemption"
  modUser = "user"
  modSetting = "setting"
)

If you adopt RawMessage (optional), apply:

-func generateDefaultSidebarConfigForRole(userRole int) string {
+func generateDefaultSidebarConfigForRole(userRole int) json.RawMessage {
@@
-  configBytes, err := json.Marshal(defaultConfig)
+  configBytes, err := json.Marshal(defaultConfig)
   if err != nil {
     common.SysLog("生成默认边栏配置失败: " + err.Error())
-    return ""
+    return nil
   }
-  return string(configBytes)
+  return configBytes
}

386-392: Initialize defaults early to avoid a second write (optional)

You can set SidebarModules in the initial defaultSetting (role is already known) and persist once.

-    defaultSetting := dto.UserSetting{}
-    // 这里暂时不设置SidebarModules,因为需要在用户创建后根据角色设置
-    user.SetSetting(defaultSetting)
+    defaultSetting := dto.UserSetting{}
+    // 可直接根据当前 user.Role 生成默认边栏配置,减少后续一次更新
+    if cfg := generateDefaultSidebarConfigForRole(user.Role); cfg != nil {
+        defaultSetting.SidebarModules = cfg // json.RawMessage if adopted; else keep string
+    }
+    user.SetSetting(defaultSetting)
web/src/components/layout/HeaderBar/Navigation.jsx (2)

24-24: Default prop to avoid undefined and ease usage (optional)

Provide a default false to pricingRequireAuth to keep call sites simple.

-const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth }) => {
+const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth = false }) => {

55-56: Preserve post-login redirect when gating pricing (optional)

Retain intended destination after login to improve UX.

-        targetPath = '/login';
+        targetPath = `/login?redirect=${encodeURIComponent(link.to)}`;

Please confirm your login page honors a redirect query param; if not, I can wire that up.

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

35-36: Merge with defaults to avoid accidental hiding when keys are missing

If HeaderNavModules omits a key, that item is currently hidden. Merge with defaults so unspecified modules inherit default visibility.

Apply:

-      const modules = headerNavModules || defaultModules;
+      const modules = headerNavModules
+        ? { ...defaultModules, ...headerNavModules }
+        : defaultModules;
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (5)

34-43: Deduplicate default config into a single constant

The default modules object is repeated 3x. Centralize to prevent drift.

+// 顶栏默认配置(集中定义,避免重复)
+const DEFAULT_HEADER_NAV_MODULES = Object.freeze({
+  home: true,
+  console: true,
+  pricing: { enabled: true, requireAuth: false },
+  docs: true,
+  about: true,
+});

-  const [headerNavModules, setHeaderNavModules] = useState({
-    home: true,
-    console: true,
-    pricing: {
-      enabled: true,
-      requireAuth: false  // 默认不需要登录鉴权
-    },
-    docs: true,
-    about: true,
-  });
+  const [headerNavModules, setHeaderNavModules] = useState(DEFAULT_HEADER_NAV_MODULES);
-  function resetHeaderNavModules() {
-    const defaultModules = {
-      home: true,
-      console: true,
-      pricing: {
-        enabled: true,
-        requireAuth: false
-      },
-      docs: true,
-      about: true,
-    };
-    setHeaderNavModules(defaultModules);
+  function resetHeaderNavModules() {
+    setHeaderNavModules(DEFAULT_HEADER_NAV_MODULES);
     showSuccess(t('已重置为默认配置'));
   }
-        // 使用默认配置
-        const defaultModules = {
-          home: true,
-          console: true,
-          pricing: {
-            enabled: true,
-            requireAuth: false
-          },
-          docs: true,
-          about: true,
-        };
-        setHeaderNavModules(defaultModules);
+        // 使用默认配置
+        setHeaderNavModules(DEFAULT_HEADER_NAV_MODULES);

Also applies to: 74-83, 140-149


46-60: Use functional setState to avoid stale closures during rapid toggles

Safer under quick interactions.

-  function handleHeaderNavModuleChange(moduleKey) {
-    return (checked) => {
-      const newModules = { ...headerNavModules };
+  function handleHeaderNavModuleChange(moduleKey) {
+    return (checked) => {
+      const newModules = { ...headerNavModules };
       if (moduleKey === 'pricing') {
         // 对于pricing模块,只更新enabled属性
         newModules[moduleKey] = {
           ...newModules[moduleKey],
           enabled: checked
         };
       } else {
         newModules[moduleKey] = checked;
       }
-      setHeaderNavModules(newModules);
+      setHeaderNavModules(() => newModules);
     };
   }

Alternatively, purely functional:

   function handleHeaderNavModuleChange(moduleKey) {
     return (checked) => {
-      const newModules = { ...headerNavModules };
-      if (moduleKey === 'pricing') {
-        newModules[moduleKey] = { ...newModules[moduleKey], enabled: checked };
-      } else {
-        newModules[moduleKey] = checked;
-      }
-      setHeaderNavModules(() => newModules);
+      setHeaderNavModules(prev => {
+        if (moduleKey === 'pricing') {
+          return { ...prev, pricing: { ...prev.pricing, enabled: checked } };
+        }
+        return { ...prev, [moduleKey]: checked };
+      });
     };
   }

63-70: Functional update for pricing auth toggle

Match the pattern above to avoid stale state.

-  function handlePricingAuthChange(checked) {
-    const newModules = { ...headerNavModules };
-    newModules.pricing = {
-      ...newModules.pricing,
-      requireAuth: checked
-    };
-    setHeaderNavModules(newModules);
-  }
+  function handlePricingAuthChange(checked) {
+    setHeaderNavModules(prev => ({
+      ...prev,
+      pricing: { ...prev.pricing, requireAuth: checked },
+    }));
+  }

116-121: Preserve original error for diagnostics

Passing a translated string to showError loses the original stack/response. Forward the actual error; UI will still show a friendly toast.

-    } catch (error) {
-      showError(t('保存失败,请重试'));
+    } catch (error) {
+      showError(error);

Optionally add a user-facing toast after logging if needed:

// showSuccess/Toast.info here if you still need a localized generic message

168-172: Copy tweak: reflect that login requirement is configurable

The description currently implies login is always required.

-      description: t('模型定价,需要登录访问'),
+      description: t('模型定价,可配置是否需要登录访问'),
web/src/hooks/common/useHeaderBar.js (2)

80-88: Be defensive when reading requireAuth

Guard against non-boolean/undefined and coerce to boolean.

-  const pricingRequireAuth = useMemo(() => {
-    if (headerNavModules?.pricing) {
-      return typeof headerNavModules.pricing === 'object'
-        ? headerNavModules.pricing.requireAuth
-        : false; // 默认不需要登录
-    }
-    return false; // 默认不需要登录
-  }, [headerNavModules]);
+  const pricingRequireAuth = useMemo(() => {
+    const p = headerNavModules?.pricing;
+    if (p && typeof p === 'object') return !!p.requireAuth;
+    return false;
+  }, [headerNavModules]);

54-79: Extract shared parser to avoid duplication with App.jsx

Both here and App.jsx parse HeaderNavModules and handle legacy booleans. Consider a helper (e.g., helpers/headerNavModules.js) exporting parseHeaderNavModules(configStr) and getPricingRequireAuth(modules).

I can draft the helper if you want it in this PR.

web/src/App.jsx (1)

59-79: De-duplicate pricingRequireAuth computation

Logic here mirrors useHeaderBar. Prefer a shared helper or import from a central util to prevent drift.

Example:

-  const pricingRequireAuth = useMemo(() => {
-    const headerNavModulesConfig = statusState?.status?.HeaderNavModules;
-    if (headerNavModulesConfig) {
-      try {
-        const modules = JSON.parse(headerNavModulesConfig);
-        if (typeof modules.pricing === 'boolean') {
-          return false;
-        }
-        return modules.pricing?.requireAuth === true;
-      } catch (error) {
-        console.error('解析顶栏模块配置失败:', error);
-        return false;
-      }
-    }
-    return false;
-  }, [statusState?.status?.HeaderNavModules]);
+  const pricingRequireAuth = useMemo(() => {
+    return getPricingRequireAuth(statusState?.status?.HeaderNavModules);
+  }, [statusState?.status?.HeaderNavModules]);

Where getPricingRequireAuth comes from a new shared helper.

web/src/components/layout/SiderBar.jsx (2)

145-188: Avoid calling isAdmin()/isRoot() inside deps; compute once per render

Calling functions in dependency arrays is brittle and harder to reason about. Compute values, use them in items and deps.

-  const adminItems = useMemo(
-    () => {
+  const admin = isAdmin();
+  const root = isRoot();
+  const adminItems = useMemo(() => {
       const items = [
         {
           text: t('渠道管理'),
           itemKey: 'channel',
           to: '/channel',
-          className: isAdmin() ? '' : 'tableHiddle',
+          className: admin ? '' : 'tableHiddle',
         },
         {
           text: t('模型管理'),
           itemKey: 'models',
           to: '/console/models',
-          className: isAdmin() ? '' : 'tableHiddle',
+          className: admin ? '' : 'tableHiddle',
         },
         {
           text: t('兑换码管理'),
           itemKey: 'redemption',
           to: '/redemption',
-          className: isAdmin() ? '' : 'tableHiddle',
+          className: admin ? '' : 'tableHiddle',
         },
         {
           text: t('用户管理'),
           itemKey: 'user',
           to: '/user',
-          className: isAdmin() ? '' : 'tableHiddle',
+          className: admin ? '' : 'tableHiddle',
         },
         {
           text: t('系统设置'),
           itemKey: 'setting',
           to: '/setting',
-          className: isRoot() ? '' : 'tableHiddle',
+          className: root ? '' : 'tableHiddle',
         },
       ];
       // 根据配置过滤项目
       const filteredItems = items.filter(item => {
         const configVisible = isModuleVisible('admin', item.itemKey);
         return configVisible;
       });
       return filteredItems;
-    },
-    [isAdmin(), isRoot(), t, isModuleVisible],
-  );
+    }, [admin, root, t, isModuleVisible]);

101-109: localStorage in deps is non-reactive; consider caching reads

Direct localStorage.getItem calls in dependency arrays won’t react to storage changes without a re-render from elsewhere. Cache into variables or state for clarity.

Example for workspaceItems:

-    [
-      localStorage.getItem('enable_data_export'),
-      localStorage.getItem('enable_drawing'),
-      localStorage.getItem('enable_task'),
-      t,
-      isModuleVisible,
-    ],
+    [t, isModuleVisible], // read localStorage inside the memo body once per render

Or lift them to useState/useEffect listening to the 'storage' event if you need cross-tab reactivity.

Also applies to: 118-141, 190-214

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

59-70: Harden adminConfig parsing and shape validation

If SidebarModulesAdmin is invalid JSON or not an object, you silently fall back; also consider non-object cases. Add a simple type guard to avoid downstream surprises.

-  const adminConfig = useMemo(() => {
+  const adminConfig = useMemo(() => {
     if (statusState?.status?.SidebarModulesAdmin) {
       try {
-        const config = JSON.parse(statusState.status.SidebarModulesAdmin);
-        return config;
+        const config = JSON.parse(statusState.status.SidebarModulesAdmin);
+        if (config && typeof config === 'object') return config;
+        return defaultAdminConfig;
       } catch (error) {
         return defaultAdminConfig;
       }
     }
     return defaultAdminConfig;
   }, [statusState?.status?.SidebarModulesAdmin]);

72-120: De-duplicate default user-config builder

The same “build defaultUserConfig” logic appears in try and catch. Factor it into a helper to reduce drift.

+  const buildDefaultUserConfig = (admin) => {
+    const cfg = {};
+    Object.keys(admin).forEach(sectionKey => {
+      if (admin[sectionKey]?.enabled) {
+        cfg[sectionKey] = { enabled: true };
+        Object.keys(admin[sectionKey]).forEach(moduleKey => {
+          if (moduleKey !== 'enabled' && admin[sectionKey][moduleKey]) {
+            cfg[sectionKey][moduleKey] = true;
+          }
+        });
+      }
+    });
+    return cfg;
+  };
@@
-      } else {
-        // 当用户没有配置时,生成一个基于管理员配置的默认用户配置
-        // 这样可以确保权限控制正确生效
-        const defaultUserConfig = {};
-        Object.keys(adminConfig).forEach(sectionKey => {
-          if (adminConfig[sectionKey]?.enabled) {
-            defaultUserConfig[sectionKey] = { enabled: true };
-            // 为每个管理员允许的模块设置默认值为true
-            Object.keys(adminConfig[sectionKey]).forEach(moduleKey => {
-              if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) {
-                defaultUserConfig[sectionKey][moduleKey] = true;
-              }
-            });
-          }
-        });
-        setUserConfig(defaultUserConfig);
-      }
+      } else {
+        setUserConfig(buildDefaultUserConfig(adminConfig));
+      }
@@
-    } catch (error) {
-      // 出错时也生成默认配置,而不是设置为空对象
-      const defaultUserConfig = {};
-      Object.keys(adminConfig).forEach(sectionKey => {
-        if (adminConfig[sectionKey]?.enabled) {
-          defaultUserConfig[sectionKey] = { enabled: true };
-          Object.keys(adminConfig[sectionKey]).forEach(moduleKey => {
-            if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) {
-              defaultUserConfig[sectionKey][moduleKey] = true;
-            }
-          });
-        }
-      });
-      setUserConfig(defaultUserConfig);
+    } catch (error) {
+      setUserConfig(buildDefaultUserConfig(adminConfig));

129-135: Prevent setState on unmounted component

Add a mounted flag to avoid state updates after unmount during slow API calls.

-  useEffect(() => {
-    // 只有当管理员配置加载完成后才加载用户配置
-    if (Object.keys(adminConfig).length > 0) {
-      loadUserConfig();
-    }
-  }, [adminConfig]);
+  useEffect(() => {
+    let active = true;
+    const run = async () => {
+      if (Object.keys(adminConfig).length > 0) {
+        await loadUserConfig();
+      }
+    };
+    if (active) run();
+    return () => { active = false; };
+  }, [adminConfig]);
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)

341-347: Coerce Switch “checked” to boolean

Avoid controlled/uncontrolled warnings when keys are absent.

-  checked={sidebarModulesUser[section.key]?.enabled}
+  checked={!!sidebarModulesUser[section.key]?.enabled}
@@
-  checked={sidebarModulesUser[section.key]?.[module.key]}
+  checked={!!sidebarModulesUser[section.key]?.[module.key]}

Also applies to: 369-375


152-178: Rename loading → saving for clarity

“loading” here indicates a save-in-progress. Rename to avoid confusion with permissionsLoading.

-  const [loading, setLoading] = useState(false);
+  const [saving, setSaving] = useState(false);
@@
-  setLoading(true);
+  setSaving(true);
@@
-  setLoading(false);
+  setSaving(false);
@@
-  loading={loading}
+  loading={saving}
web/src/components/settings/personal/cards/NotificationSettings.jsx (3)

154-157: Guard adminConfig parse

Same as above—protect JSON.parse with try/catch and type checks.

-        if (statusState?.status?.SidebarModulesAdmin) {
-          const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
-          setAdminConfig(adminConf);
+        if (statusState?.status?.SidebarModulesAdmin) {
+          try {
+            const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
+            if (adminConf && typeof adminConf === 'object') setAdminConfig(adminConf);
+          } catch {}
         }

602-606: Coerce Switch “checked” props to booleans

Prevents UI warnings when state keys are absent.

- checked={sidebarModulesUser[section.key]?.enabled}
+ checked={!!sidebarModulesUser[section.key]?.enabled}
@@
- checked={sidebarModulesUser[section.key]?.[module.key]}
+ checked={!!sidebarModulesUser[section.key]?.[module.key]}

Also applies to: 642-646


122-137: Avoid duplicated sidebar editor logic

This component reimplements the editor already added in SettingsSidebarModulesUser and logic now lives in useSidebar. Consider extracting a shared SidebarSettingsPanel or reuse the hook to DRY parsing, defaults, filtering, and saving.

Also applies to: 275-301, 539-658

controller/user.go (1)

505-566: generateDefaultSidebarConfig placement

This helper belongs in model/service to avoid duplication with model-side defaults; otherwise it’s dead code here. Consider moving or removing.

📜 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 6a87808 and 8809c44.

📒 Files selected for processing (19)
  • controller/misc.go (1 hunks)
  • controller/user.go (5 hunks)
  • dto/user_settings.go (1 hunks)
  • model/user.go (2 hunks)
  • web/src/App.jsx (4 hunks)
  • web/src/components/layout/HeaderBar/Navigation.jsx (2 hunks)
  • web/src/components/layout/HeaderBar/index.jsx (3 hunks)
  • web/src/components/layout/SiderBar.jsx (4 hunks)
  • web/src/components/settings/OperationSetting.jsx (3 hunks)
  • web/src/components/settings/PersonalSetting.jsx (2 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (7 hunks)
  • web/src/hooks/common/useHeaderBar.js (3 hunks)
  • web/src/hooks/common/useNavigation.js (1 hunks)
  • web/src/hooks/common/useSidebar.js (1 hunks)
  • web/src/hooks/common/useUserPermissions.js (1 hunks)
  • web/src/i18n/locales/en.json (1 hunks)
  • web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1 hunks)
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1 hunks)
  • web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
web/src/components/settings/OperationSetting.jsx (2)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
  • SettingsHeaderNavModules (28-326)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
  • SettingsSidebarModulesAdmin (28-362)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (2)
web/src/hooks/common/useHeaderBar.js (1)
  • headerNavModules (58-78)
web/src/helpers/utils.jsx (2)
  • showSuccess (153-155)
  • showError (118-147)
controller/misc.go (1)
common/constants.go (1)
  • OptionMap (36-36)
web/src/components/layout/HeaderBar/Navigation.jsx (2)
web/src/App.jsx (1)
  • pricingRequireAuth (60-79)
web/src/hooks/common/useHeaderBar.js (1)
  • pricingRequireAuth (81-88)
web/src/hooks/common/useUserPermissions.js (2)
web/src/hooks/common/useHeaderBar.js (1)
  • loading (42-42)
web/src/hooks/common/useSidebar.js (1)
  • loading (27-27)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (5)
  • Typography (30-30)
  • useTranslation (33-33)
  • loading (34-34)
  • statusState (35-35)
  • sectionConfigs (250-304)
web/src/helpers/utils.jsx (2)
  • showSuccess (153-155)
  • showError (118-147)
web/src/hooks/common/useSidebar.js (1)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (4)
  • useSidebar (47-47)
  • statusState (35-35)
  • loading (34-34)
  • adminConfig (112-112)
model/user.go (4)
common/constants.go (2)
  • RoleAdminUser (135-135)
  • RoleRootUser (136-136)
common/sys_log.go (1)
  • SysLog (10-13)
dto/user_settings.go (1)
  • UserSetting (3-12)
model/main.go (1)
  • DB (63-63)
web/src/components/settings/personal/cards/NotificationSettings.jsx (4)
web/src/App.jsx (1)
  • statusState (57-57)
web/src/hooks/common/useSidebar.js (2)
  • statusState (25-25)
  • adminConfig (60-70)
web/src/hooks/common/useUserPermissions.js (6)
  • useUserPermissions (8-98)
  • useUserPermissions (8-98)
  • error (11-11)
  • isSidebarSectionAllowed (45-49)
  • isSidebarModuleAllowed (52-63)
  • hasSidebarSettingsPermission (40-42)
web/src/helpers/utils.jsx (2)
  • showSuccess (153-155)
  • showError (118-147)
web/src/App.jsx (2)
web/src/hooks/common/useHeaderBar.js (3)
  • statusState (34-34)
  • pricingRequireAuth (81-88)
  • headerNavModulesConfig (55-55)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
  • statusState (31-31)
web/src/hooks/common/useNavigation.js (3)
web/src/components/layout/HeaderBar/index.jsx (1)
  • useNavigation (65-65)
web/src/hooks/common/useHeaderBar.js (2)
  • docsLink (51-51)
  • headerNavModules (58-78)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
  • headerNavModules (34-43)
web/src/hooks/common/useHeaderBar.js (2)
web/src/App.jsx (2)
  • statusState (57-57)
  • pricingRequireAuth (60-79)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (2)
  • statusState (31-31)
  • headerNavModules (34-43)
controller/user.go (5)
common/constants.go (3)
  • RoleCommonUser (134-134)
  • RoleRootUser (136-136)
  • RoleAdminUser (135-135)
common/json.go (2)
  • Marshal (20-22)
  • Unmarshal (8-10)
common/sys_log.go (1)
  • SysLog (10-13)
model/user.go (2)
  • GetUserById (287-299)
  • User (19-48)
common/gin.go (1)
  • ApiError (95-100)
web/src/components/layout/SiderBar.jsx (3)
web/src/hooks/common/useSidebar.js (4)
  • useSidebar (24-220)
  • useSidebar (24-220)
  • isModuleVisible (183-189)
  • hasSectionVisibleModules (192-199)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
  • useSidebar (47-47)
web/src/helpers/utils.jsx (2)
  • isAdmin (35-40)
  • isRoot (42-47)
web/src/components/layout/HeaderBar/index.jsx (4)
web/src/hooks/common/useNavigation.js (2)
  • useNavigation (22-88)
  • useNavigation (22-88)
web/src/hooks/common/useHeaderBar.js (3)
  • docsLink (51-51)
  • headerNavModules (58-78)
  • pricingRequireAuth (81-88)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
  • headerNavModules (34-43)
web/src/App.jsx (1)
  • pricingRequireAuth (60-79)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (3)
web/src/hooks/common/useSidebar.js (6)
  • loading (27-27)
  • statusState (25-25)
  • useSidebar (24-220)
  • useSidebar (24-220)
  • adminConfig (60-70)
  • refreshUserConfig (123-127)
web/src/hooks/common/useUserPermissions.js (7)
  • loading (10-10)
  • useUserPermissions (8-98)
  • useUserPermissions (8-98)
  • hasSidebarSettingsPermission (40-42)
  • isSidebarSectionAllowed (45-49)
  • isSidebarModuleAllowed (52-63)
  • error (11-11)
web/src/helpers/utils.jsx (2)
  • showSuccess (153-155)
  • showError (118-147)
🪛 Biome (2.1.2)
web/src/i18n/locales/en.json

[error] 2064-2064: The key 操练场 was already declared.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

[error] 120-120: 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] 124-124: 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] 197-197: 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 (13)
web/src/i18n/locales/en.json (1)

2021-2078: Ignore duplicate‐keys suggestion – a scan of web/src/i18n/locales/en.json shows no repeated keys; the linter error no longer applies.

Likely an incorrect or invalid review comment.

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

41-42: Whitespace-only changes are fine.

No functional impact. LGTM.

Also applies to: 335-336

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

23-24: LGTM: new settings panels imported correctly


51-56: LGTM: state keys for new options are wired
Values come from /api/option; empty-string defaults are acceptable since children parse JSON.

web/src/components/layout/HeaderBar/index.jsx (3)

47-49: LGTM: header config and auth flag plumbed from hook


65-65: LGTM: useNavigation now correctly receives headerNavModules


107-107: LGTM: pricingRequireAuth forwarded to Navigation

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

100-107: Dispatch payload shape matches reducer contract
The statusReducer’s 'set' case applies action.payload directly to state.status, so passing the flattened status object is correct.

web/src/App.jsx (1)

278-291: Clarify behavior when pricing module is disabled

Currently, the /pricing route remains accessible even if modules.pricing.enabled is false (only the header link hides). Confirm product intent: should the route also be disabled/redirected when admin turns it off?

If it should be blocked, I can provide a small guard that redirects to 404 or Home when disabled.

web/src/components/layout/SiderBar.jsx (1)

258-261: Good: consistent error surface for chat parsing

Switching to showError('聊天数据解析失败') keeps UX consistent and avoids noisy console logs.

controller/user.go (3)

213-214: Explicit default role assignment on Register looks good

Setting Role to common.RoleCommonUser during registration is correct and prevents privilege escalation at sign-up.


428-473: GetSelf: include permissions and sidebar_modules — LGTM

The response shape aligns with frontend needs and hides remark. Keep it stable for clients.


856-861: CreateUser preserves role — LGTM

Respects admin-provided role with upstream checks already in place.

Comment thread controller/misc.go
Comment on lines +92 to +95
// 模块管理配置
"HeaderNavModules": common.OptionMap["HeaderNavModules"],
"SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Data race on common.OptionMap reads in GetStatus.

OptionMap is a map guarded elsewhere with OptionMapRWMutex; reading it here without the lock can race with writers.

Apply:

-        // 模块管理配置
-        "HeaderNavModules":     common.OptionMap["HeaderNavModules"],
-        "SidebarModulesAdmin":  common.OptionMap["SidebarModulesAdmin"],
+        // 模块管理配置(下方加锁后设置)

Then, after data is constructed:

   }
 
+  // 模块管理配置(加读锁)
+  common.OptionMapRWMutex.RLock()
+  data["HeaderNavModules"] = common.OptionMap["HeaderNavModules"]
+  data["SidebarModulesAdmin"] = common.OptionMap["SidebarModulesAdmin"]
+  common.OptionMapRWMutex.RUnlock()
+
   // 根据启用状态注入可选内容
🤖 Prompt for AI Agents
In controller/misc.go around lines 92-95, reads of common.OptionMap
(HeaderNavModules and SidebarModulesAdmin) are unprotected and can race with
writers; wrap the reads with common.OptionMapRWMutex.RLock() before accessing
OptionMap and RUnlock() afterwards, either by copying the needed values into
local variables under the read-lock or by populating the response map entries
while the lock is held, then release the lock once those values have been
captured/inserted.

Comment thread controller/user.go
Comment on lines +655 to +697
var requestData map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&requestData)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
return
}

// 检查是否是sidebar_modules更新请求
if sidebarModules, exists := requestData["sidebar_modules"]; exists {
userId := c.GetInt("id")
user, err := model.GetUserById(userId, false)
if err != nil {
common.ApiError(c, err)
return
}

// 获取当前用户设置
currentSetting := user.GetSetting()

// 更新sidebar_modules字段
if sidebarModulesStr, ok := sidebarModules.(string); ok {
currentSetting.SidebarModules = sidebarModulesStr
}

// 保存更新后的设置
user.SetSetting(currentSetting)
if err := user.Update(false); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "更新设置失败: " + err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "设置更新成功",
})
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

UpdateSelf lacks server-side authorization and input validation for sidebar_modules

Anyone can POST sidebar_modules regardless of role; also raw strings are accepted without JSON validation. Enforce RBAC and validate JSON; accept object input too.

 func UpdateSelf(c *gin.Context) {
   var requestData map[string]interface{}
   err := json.NewDecoder(c.Request.Body).Decode(&requestData)
@@
   // 检查是否是sidebar_modules更新请求
   if sidebarModules, exists := requestData["sidebar_modules"]; exists {
-    userId := c.GetInt("id")
+    userId := c.GetInt("id")
+    userRole := c.GetInt("role")
+    // 基于后端权限控制,避免绕过前端限制
+    perms := calculateUserPermissions(userRole)
+    if allow, ok := perms["sidebar_settings"].(bool); !ok || !allow {
+      c.JSON(http.StatusOK, gin.H{
+        "success": false,
+        "message": "无权更新边栏设置",
+      })
+      return
+    }
     user, err := model.GetUserById(userId, false)
@@
-    // 更新sidebar_modules字段
-    if sidebarModulesStr, ok := sidebarModules.(string); ok {
-      currentSetting.SidebarModules = sidebarModulesStr
-    }
+    // 更新sidebar_modules字段(支持字符串或对象)
+    switch v := sidebarModules.(type) {
+    case string:
+      // 验证JSON格式
+      var tmp map[string]interface{}
+      if err := json.Unmarshal([]byte(v), &tmp); err != nil {
+        c.JSON(http.StatusOK, gin.H{"success": false, "message": "无效的边栏配置"})
+        return
+      }
+      currentSetting.SidebarModules = v
+    case map[string]interface{}:
+      // 统一存为字符串
+      b, _ := json.Marshal(v)
+      currentSetting.SidebarModules = string(b)
+    default:
+      c.JSON(http.StatusOK, gin.H{"success": false, "message": "无效的边栏配置类型"})
+      return
+    }

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

🤖 Prompt for AI Agents
In controller/user.go around lines 655-697, enforce server-side RBAC and
stricter JSON validation for sidebar_modules: before loading the target user,
verify the requester is authorized (e.g., requester ID equals target ID OR
requester has an admin role/permission via c.GetInt("id") vs target id and
c.GetString("role") or model permission check) and return 403 if not allowed;
then accept sidebar_modules as either a JSON string or an object/array — if it's
a string attempt to json.Unmarshal to a concrete structure (e.g., []string or
map[string]bool) to validate contents, if it's an object/array validate its
shape and items, sanitize/whitelist allowed module keys, enforce size limits,
then json.Marshal the validated structure to store as the setting string; on any
validation error return 400 with an explanatory message and do not update,
otherwise set the new setting and persist as before.

Comment thread model/user.go
Comment on lines +398 to +411
// 用户创建成功后,根据角色初始化边栏配置
// 需要重新获取用户以确保有正确的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))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid re-query by username; update by ID and handle errors

Re-fetching by username is unnecessary and error-prone. Also, errors from Update are ignored.

-  // 用户创建成功后,根据角色初始化边栏配置
-  // 需要重新获取用户以确保有正确的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))
-    }
-  }
+  // 用户创建成功后,根据角色初始化边栏配置(使用已获取的 user.Id / user.Role)
+  if cfg := generateDefaultSidebarConfigForRole(user.Role); cfg != nil {
+    current := user.GetSetting()
+    current.SidebarModules = cfg // json.RawMessage if adopted; else keep string
+    user.SetSetting(current)
+    if err := DB.Model(&User{}).Where("id = ?", user.Id).Update("setting", user.Setting).Error; err != nil {
+      common.SysLog("初始化边栏配置失败: " + err.Error())
+    } else {
+      common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", user.Username, user.Role))
+    }
+  }

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

🤖 Prompt for AI Agents
In model/user.go around lines 398 to 411, remove the re-query by username and
instead load/update the record by the new user's ID (e.g.,
DB.First(&createdUser, user.ID) or use the created user instance that has the
ID), set the role-based SidebarModules on that record, call Update and check its
returned error (handle/log/return it instead of ignoring), and ensure DB
lookup/update use the primary key to avoid ambiguity and race conditions.

Comment on lines +154 to +164
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
}

// 获取用户个人配置
const userRes = await API.get('/api/user/self');
if (userRes.data.success && userRes.data.data.sidebar_modules) {
const userConf = JSON.parse(userRes.data.data.sidebar_modules);
setSidebarModulesUser(userConf);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle non-string sidebar_modules and parse failures

APIs may return an object or malformed JSON. Add guards to avoid runtime crashes.

-        if (userRes.data.success && userRes.data.data.sidebar_modules) {
-          const userConf = JSON.parse(userRes.data.data.sidebar_modules);
-          setSidebarModulesUser(userConf);
+        if (userRes.data.success && userRes.data.data.sidebar_modules) {
+          let userConf = userRes.data.data.sidebar_modules;
+          try {
+            if (typeof userConf === 'string') userConf = JSON.parse(userConf);
+            if (userConf && typeof userConf === 'object') setSidebarModulesUser(userConf);
+          } catch (e) {
+            console.warn('用户边栏配置解析失败,保留默认:', e);
+          }
         }

Also applies to: 160-164

🤖 Prompt for AI Agents
In web/src/components/settings/personal/cards/NotificationSettings.jsx around
lines 154 to 164, the code blindly calls JSON.parse on
statusState.status.SidebarModulesAdmin and userRes.data.data.sidebar_modules
which can be non-strings or malformed JSON; update the logic to (1) check typeof
value === 'string' before parsing, (2) if the value is already an object use it
directly, (3) wrap JSON.parse in try/catch to handle parse failures and fall
back to a safe default (e.g., {} or skip setting), and (4) verify the resulting
value is an object before calling setAdminConfig or setSidebarModulesUser so
runtime crashes are avoided.

Comment on lines +75 to +79
if (link.itemKey === 'pricing') {
// 支持新的pricing配置格式
return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing;
}
return modules[link.itemKey] === true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden pricing check to avoid null-dereference

typeof null === 'object'. If pricing is null, reading .enabled throws. Guard for truthiness and coerce to boolean.

-        if (link.itemKey === 'pricing') {
-          // 支持新的pricing配置格式
-          return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing;
-        }
+        if (link.itemKey === 'pricing') {
+          // 支持新的pricing配置格式并防御 null
+          return modules.pricing && typeof modules.pricing === 'object'
+            ? !!modules.pricing.enabled
+            : !!modules.pricing;
+        }
📝 Committable suggestion

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

Suggested change
if (link.itemKey === 'pricing') {
// 支持新的pricing配置格式
return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing;
}
return modules[link.itemKey] === true;
if (link.itemKey === 'pricing') {
// 支持新的pricing配置格式并防御 null
return modules.pricing && typeof modules.pricing === 'object'
? !!modules.pricing.enabled
: !!modules.pricing;
}
return modules[link.itemKey] === true;
🤖 Prompt for AI Agents
In web/src/hooks/common/useNavigation.js around lines 75 to 79, the pricing
check uses typeof modules.pricing === 'object' which treats null as 'object' and
leads to a null-dereference when accessing .enabled; change the guard to first
ensure modules.pricing is truthy (e.g., modules.pricing && typeof
modules.pricing === 'object') and then return the boolean value (coerce enabled
or the pricing value to a true/false) so null/undefined are handled safely.

Comment on lines +45 to +49
const isSidebarSectionAllowed = (sectionKey) => {
if (!permissions?.sidebar_modules) return true;
const sectionPerms = permissions.sidebar_modules[sectionKey];
return sectionPerms !== false;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Section “enabled” flag is ignored → modules/sections can render when disabled.

Admin config uses { enabled: false, ... }, but checks here only compare sectionPerms === false. Respect the enabled flag at both section and module levels.

Apply:

-  const isSidebarSectionAllowed = (sectionKey) => {
-    if (!permissions?.sidebar_modules) return true;
-    const sectionPerms = permissions.sidebar_modules[sectionKey];
-    return sectionPerms !== false;
-  };
+  const isSidebarSectionAllowed = (sectionKey) => {
+    if (!permissions?.sidebar_modules) return true;
+    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) => {
+  const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
     if (!permissions?.sidebar_modules) return true;
     const sectionPerms = permissions.sidebar_modules[sectionKey];
-    
-    // 如果整个区域被禁用
-    if (sectionPerms === false) return false;
+    // 如果整个区域被禁用
+    if (sectionPerms === false) return false;
+    if (sectionPerms && typeof sectionPerms === 'object' && sectionPerms.enabled === false) return false;
     
     // 如果区域存在但模块被禁用
     if (sectionPerms && sectionPerms[moduleKey] === false) return false;
     
     return true;
   };
...
-  const getAllowedSidebarSections = () => {
-    if (!permissions?.sidebar_modules) return [];
-    
-    return Object.keys(permissions.sidebar_modules).filter(sectionKey => 
-      isSidebarSectionAllowed(sectionKey)
-    );
-  };
+  const getAllowedSidebarSections = () => {
+    if (!permissions?.sidebar_modules) return [];
+    return Object.keys(permissions.sidebar_modules).filter(isSidebarSectionAllowed);
+  };
...
-  const getAllowedSidebarModules = (sectionKey) => {
+  const getAllowedSidebarModules = (sectionKey) => {
     if (!permissions?.sidebar_modules) return [];
     const sectionPerms = permissions.sidebar_modules[sectionKey];
     
-    if (sectionPerms === false) return [];
+    if (sectionPerms === false) return [];
+    if (sectionPerms && typeof sectionPerms === 'object' && sectionPerms.enabled === false) return [];
     if (!sectionPerms || typeof sectionPerms !== 'object') return [];
     
     return Object.keys(sectionPerms).filter(moduleKey => 
       moduleKey !== 'enabled' && sectionPerms[moduleKey] === true
     );
   };

Also applies to: 52-63, 66-72, 75-85

🤖 Prompt for AI Agents
In web/src/hooks/common/useUserPermissions.js around lines 45-49 (and also apply
the same fix to 52-63, 66-72, 75-85), the code currently treats a section as
disabled only when the value is strictly false and ignores objects like {
enabled: false, ... }. Update each permission check to treat a section/module as
disallowed when the permission entry is strictly false OR when it's an object
with enabled === false (e.g., sectionPerms === false || sectionPerms?.enabled
=== false), and preserve the current behavior of allowing access when the
permission entry is undefined; apply this same enabled check at both section and
module levels wherever similar logic appears in the referenced line ranges.

Comment on lines +49 to +57
// 如果没有边栏设置权限,不显示此组件
if (!permissionsLoading && !hasSidebarSettingsPermission()) {
return null;
}

// 权限加载中,显示加载状态
if (permissionsLoading) {
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix conditional hook order violations (early returns before hooks)

Hooks must run unconditionally. Move these early returns below all hooks (after useState/useEffect) or gate inside render.

-  // 如果没有边栏设置权限,不显示此组件
-  if (!permissionsLoading && !hasSidebarSettingsPermission()) {
-    return null;
-  }
-
-  // 权限加载中,显示加载状态
-  if (permissionsLoading) {
-    return null;
-  }
+  // ... keep all hooks above ...
+  // Defer early returns until after all hooks are declared
+  const shouldHide = permissionsLoading || !hasSidebarSettingsPermission();
+  if (shouldHide) return null;
📝 Committable suggestion

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

Suggested change
// 如果没有边栏设置权限,不显示此组件
if (!permissionsLoading && !hasSidebarSettingsPermission()) {
return null;
}
// 权限加载中,显示加载状态
if (permissionsLoading) {
return null;
}
// …keep all hooks above…
// Defer early returns until after all hooks are declared
const shouldHide = permissionsLoading || !hasSidebarSettingsPermission();
if (shouldHide) return null;
🤖 Prompt for AI Agents
In web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx around lines
49-57, the current early returns for permission/loading state run before React
hooks causing hook-order violations; move these conditional returns so all hooks
(useState/useEffect/useMemo/useContext etc.) are called first, then check
permissionsLoading and hasSidebarSettingsPermission() to return null after hooks
have executed, or alternatively keep the returns but wrap them inside the
component's render JSX (e.g., conditional rendering) rather than exiting before
hooks.

Comment on lines +181 to +236
useEffect(() => {
const loadConfigs = async () => {
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
}

// 获取用户个人配置
const userRes = await API.get('/api/user/self');
if (userRes.data.success && userRes.data.data.sidebar_modules) {
let userConf;
// 检查sidebar_modules是字符串还是对象
if (typeof userRes.data.data.sidebar_modules === 'string') {
userConf = JSON.parse(userRes.data.data.sidebar_modules);
} else {
userConf = userRes.data.data.sidebar_modules;
}
console.log('从API加载的用户配置:', userConf);

// 确保用户配置也经过权限过滤
const filteredUserConf = {};
Object.keys(userConf).forEach(sectionKey => {
if (isSidebarSectionAllowed(sectionKey)) {
filteredUserConf[sectionKey] = { ...userConf[sectionKey] };
// 过滤不允许的模块
Object.keys(userConf[sectionKey]).forEach(moduleKey => {
if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) {
delete filteredUserConf[sectionKey][moduleKey];
}
});
}
});
setSidebarModulesUser(filteredUserConf);
console.log('权限过滤后的用户配置:', filteredUserConf);
} else {
// 如果用户没有配置,使用权限过滤后的默认配置
const defaultConfig = generateDefaultConfig();
setSidebarModulesUser(defaultConfig);
console.log('用户无配置,使用默认配置:', defaultConfig);
}
} catch (error) {
console.error('加载边栏配置失败:', error);
// 出错时也使用默认配置
const defaultConfig = generateDefaultConfig();
setSidebarModulesUser(defaultConfig);
}
};

// 只有权限加载完成且有边栏设置权限时才加载配置
if (!permissionsLoading && hasSidebarSettingsPermission()) {
loadConfigs();
}
}, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Effect re-renders due to unstable function deps

Depending on function refs from useUserPermissions causes reload loops. Depend on stable values (permissions) or memoize functions in the hook.

-  }, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]);
+  }, [statusState, permissionsLoading, permissions]);

Optionally, memoize functions inside useUserPermissions with useCallback for stability.

📝 Committable suggestion

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

Suggested change
useEffect(() => {
const loadConfigs = async () => {
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
}
// 获取用户个人配置
const userRes = await API.get('/api/user/self');
if (userRes.data.success && userRes.data.data.sidebar_modules) {
let userConf;
// 检查sidebar_modules是字符串还是对象
if (typeof userRes.data.data.sidebar_modules === 'string') {
userConf = JSON.parse(userRes.data.data.sidebar_modules);
} else {
userConf = userRes.data.data.sidebar_modules;
}
console.log('从API加载的用户配置:', userConf);
// 确保用户配置也经过权限过滤
const filteredUserConf = {};
Object.keys(userConf).forEach(sectionKey => {
if (isSidebarSectionAllowed(sectionKey)) {
filteredUserConf[sectionKey] = { ...userConf[sectionKey] };
// 过滤不允许的模块
Object.keys(userConf[sectionKey]).forEach(moduleKey => {
if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) {
delete filteredUserConf[sectionKey][moduleKey];
}
});
}
});
setSidebarModulesUser(filteredUserConf);
console.log('权限过滤后的用户配置:', filteredUserConf);
} else {
// 如果用户没有配置,使用权限过滤后的默认配置
const defaultConfig = generateDefaultConfig();
setSidebarModulesUser(defaultConfig);
console.log('用户无配置,使用默认配置:', defaultConfig);
}
} catch (error) {
console.error('加载边栏配置失败:', error);
// 出错时也使用默认配置
const defaultConfig = generateDefaultConfig();
setSidebarModulesUser(defaultConfig);
}
};
// 只有权限加载完成且有边栏设置权限时才加载配置
if (!permissionsLoading && hasSidebarSettingsPermission()) {
loadConfigs();
}
}, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]);
useEffect(() => {
const loadConfigs = async () => {
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
}
// 获取用户个人配置
const userRes = await API.get('/api/user/self');
if (userRes.data.success && userRes.data.data.sidebar_modules) {
let userConf;
// 检查sidebar_modules是字符串还是对象
if (typeof userRes.data.data.sidebar_modules === 'string') {
userConf = JSON.parse(userRes.data.data.sidebar_modules);
} else {
userConf = userRes.data.data.sidebar_modules;
}
console.log('从API加载的用户配置:', userConf);
// 确保用户配置也经过权限过滤
const filteredUserConf = {};
Object.keys(userConf).forEach(sectionKey => {
if (isSidebarSectionAllowed(sectionKey)) {
filteredUserConf[sectionKey] = { ...userConf[sectionKey] };
// 过滤不允许的模块
Object.keys(userConf[sectionKey]).forEach(moduleKey => {
if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) {
delete filteredUserConf[sectionKey][moduleKey];
}
});
}
});
setSidebarModulesUser(filteredUserConf);
console.log('权限过滤后的用户配置:', filteredUserConf);
} else {
// 如果用户没有配置,使用权限过滤后的默认配置
const defaultConfig = generateDefaultConfig();
setSidebarModulesUser(defaultConfig);
console.log('用户无配置,使用默认配置:', defaultConfig);
}
} catch (error) {
console.error('加载边栏配置失败:', error);
// 出错时也使用默认配置
const defaultConfig = generateDefaultConfig();
setSidebarModulesUser(defaultConfig);
}
};
// 只有权限加载完成且有边栏设置权限时才加载配置
if (!permissionsLoading && hasSidebarSettingsPermission()) {
loadConfigs();
}
}, [statusState, permissionsLoading, permissions]);
🧰 Tools
🪛 Biome (2.1.2)

[error] 197-197: 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)

🤖 Prompt for AI Agents
In web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx around lines
181-236, the useEffect depends on unstable function references
isSidebarSectionAllowed and isSidebarModuleAllowed which causes re-renders;
replace these function references in the dependency array with stable values
derived from permissions (e.g., a memoized permissions object/array or specific
permission booleans) or ensure those functions are memoized at their source
(inside useUserPermissions) with useCallback so their identity is stable; update
the effect deps to only include statusState, permissionsLoading,
hasSidebarSettingsPermission and the stable memoized permission values (or
memoized functions), and/or memoize any computed default/filtered configs used
inside the effect to prevent reload loops.

Comment on lines +185 to +188
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard JSON.parse for adminConfig

A malformed SidebarModulesAdmin will crash the page. Wrap in try/catch with fallback.

-        if (statusState?.status?.SidebarModulesAdmin) {
-          const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
-          setAdminConfig(adminConf);
-          console.log('加载管理员边栏配置:', adminConf);
+        if (statusState?.status?.SidebarModulesAdmin) {
+          try {
+            const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
+            if (adminConf && typeof adminConf === 'object') {
+              setAdminConfig(adminConf);
+              console.log('加载管理员边栏配置:', adminConf);
+            }
+          } catch (e) {
+            console.warn('管理员边栏配置解析失败,使用空配置:', e);
+            setAdminConfig({});
+          }
         }
📝 Committable suggestion

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

Suggested change
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
if (statusState?.status?.SidebarModulesAdmin) {
try {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
if (adminConf && typeof adminConf === 'object') {
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
}
} catch (e) {
console.warn('管理员边栏配置解析失败,使用空配置:', e);
setAdminConfig({});
}
}
🤖 Prompt for AI Agents
In web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx around lines
185 to 188, the code directly JSON.parse's
statusState.status.SidebarModulesAdmin which will throw on malformed JSON; wrap
the parse in a try/catch, on success call setAdminConfig with the parsed object
and keep the console.log, on failure setAdminConfig to a safe fallback (e.g., {}
or existing default) and optionally log a warning/error with the raw value;
ensure you only call JSON.parse if the value is a non-empty string and avoid
letting the exception bubble up.

@creamlike1024
creamlike1024 merged commit 153994f into QuantumNous:alpha Aug 31, 2025
1 check passed
This was referenced Sep 11, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Sep 30, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Nov 24, 2025
@coderabbitai coderabbitai Bot mentioned this pull request May 1, 2026
11 tasks
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.

2 participants