feat: 新增邀请开关与分组管理,并修复权限同步及UI样式问题 - #1703
Conversation
WalkthroughAdds 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
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
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()
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Possibly related PRs
Pre-merge checks (3 passed)✅ Passed checks (3 passed)
Poem
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.
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 detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this comment.
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 functionThis 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 dropNo functional impact. Consider avoiding formatting-only diffs to keep history clean.
model/main.go (1)
133-134: Formatting-only changeNo 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 statementsCommented 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; + }注意:当前非布尔字段会以字符串形式保存到后端。如果后端对数值键(如
RetryTimes、USDExchangeRate)要求为数字,请确认 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.
📒 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 LGTMImproved Chinese phrasing reads better and matches i18n usage.
setting/operation_setting/general_setting.go (1)
9-10: Approve invitation feature flag wiringAll 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 逻辑分歧。
There was a problem hiding this comment.
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 rejectionsNetwork 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 defaultSame 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 spinnerWrap 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 backgroundCurrent 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-hiddenAvoid 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 childrenLightweight 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 submitUse 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 booleanAvoid relying on truthy strings.
- const isValidResetLink = email && token; + const isValidResetLink = Boolean(email && token);
71-83: Optimize countdown interval to avoid re-creating timer every tickUse 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 handlersLet 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 statusInvalid 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 pageReduce 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 groupsCurrently 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 modalallowAdditions 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 payloadThe 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 logicSame 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 objectdefaultModules 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 handlerInline 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-switchRedundant 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 expressionNo 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 comparisonEnsure 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 deletionAfter 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 translationSame 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.
📒 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: importsAuthPageLayout import is correct and localized to auth components.
607-634: Verify scroll behavior with new wrapperAuthPageLayout 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 openFetching 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 groupsUsing 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 configurationThe nested structure (enabled + groupManagement) is a good foundation for fine-grained gating.
94-107: LGTM: isolated handler for groupManagementClear, localized state update for the nested flag.
web/src/components/table/users/UsersActions.jsx (1)
100-107: LGTM: modal gated with same predicate as buttonKeeps render consistent with the permission check.
There was a problem hiding this comment.
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 reloadsGreat 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 selfDispatching 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 handlerWithout 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.
📒 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
There was a problem hiding this comment.
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 fillsuser.Id) and update only thesettingcolumn 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)whereuseris*Userpasses**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 shadowingsettingand simplify the cache branchReplace the short-declaration in the Redis check with a new local name and return the cached
dto.UserSettingdirectly, for example:if !fromDB && common.RedisEnabled { cached, err := getUserSettingCache(id) if err == nil { return cached, nil } // fall through to DB }No wrapping in
UserBase.GetSettingis required sincegetUserSettingCachealready returnsdto.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 == newGroupNameor 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
commonGroupColcan 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 consistencyUse 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 environmentsIf 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 fineUpdates 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 renderDefine 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 booleansThe 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
>= 100for super-admin and>= 10 && < 100for admin, while backend checks equality againstcommon.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 !== 2returns false. Optionally log once to help diagnose misconfiguredmodulePath.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.
📒 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 showSuccessweb/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 correctDirectly 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; goodKeeps backward-compatible defaults while exposing the new control.
213-218: Fallback default on load error includes nested user; goodPrevents shape drift when options parsing fails.
model/user.go (1)
96-96: All call sites updated and no dead references remain. Verified that the unexportedgenerateDefaultSidebarConfigForRoleis no longer referenced and all calls useGenerateDefaultSidebarConfigForRole.
There was a problem hiding this comment.
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/*.jsonweb/src/components/topup/index.jsx (1)
265-301: Unsafe fallback: enabling invitation on failure leaks UI and triggers failing callsOn /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 0Currently 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 renameRename 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 absentToday 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 instanceIdEmit 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 || fallbackUse 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.0Parse 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 titleMirror 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 statementsReplace 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 nameDuring 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 loadingAdmin 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 editscurrentInputs 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 firstIf 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 errorUse 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 configThis 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 objectsAvoid 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 checksRely 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 renderAvoid 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 filteringisUserModuleAllowed 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 troubleshootingYou 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 existsGiven 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 coordinationIf 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 configSilent "{}" 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 filterSymmetry 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 driftisUserModuleAllowedInFilter 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 shapeAdd 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 divergenceThe 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 loadingCurrently 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 loadingImproves 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
getGroupDescriptionlogic fromweb/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
mergeUserConfigWithSystemConfigfunction 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.
📒 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: thesetreducer replacesstate.statuswith your payload (not wrapped) anduseHeaderBarJSON.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 goodGrid 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 soundCovers 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 saverefreshUserConfig ensures immediate UI consistency. LGTM.
138-141: LGTM: trigger refresh post-saveKeeps other views in sync via the shared hook.
728-729: Copy/text change is finei18n string reads well.
router/api-router.go (1)
81-90: LGTM: per-module middleware placement is consistent and minimalModuleAuth 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 configMatches frontend expectations and reduces leakage of disabled items.
45-50: LGTM: role derivation and invitation_enabled exposureRole 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 defaultsHook 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 booleanIf 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 (
TransferAffQuotaandGetAffCode) with the centralizedInvitationEnabledsetting 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.
There was a problem hiding this comment.
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 wrongAPI 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 **/*.goAlso 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:falsecheckNestedPermission 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, andconfigLoadingto 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 contextsAvoid 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 inputPrevents 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().Errorweb/src/hooks/model-pricing/useModelPricingData.jsx (3)
178-193: Combine the two sorts into a single stable comparatorTwo 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 redirectsSet 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 clarityNot 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 unusedEither 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 neededIf 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 localisRoot/isAdmindefinitions with the shared helpers fromweb/src/constants/user.constants.jsin:
- 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: Parsesettingonce 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.
📒 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 — LGTMThis 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 516model/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 goodModuleAuth 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: Confirmadmin.usershape change is propagated
Ran repo-wide searches for any code treatingadmin.useras 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 handlessidebar_modules.
TheUpdateSelfhandler detects thesidebar_modulesfield, merges it into the user’ssettingJSON viauser.SetSetting(...), and saves it withuser.Update(). No silent drops—no change required.
There was a problem hiding this comment.
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
UserGroupcurrently uses a partial unique index (where:deleted_at IS NULL) in model/user_group.go. Replace it with a composite unique index shared byNameandDeletedAt.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
ModuleRoutewrappingSuspensewrapping 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.
📒 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.jsweb/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsxweb/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
useNavigatefor 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
invitationEnabledas 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 && invitationEnabledproperly 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
ModuleRouteimport to enable fine-grained module access control.
62-80: Correct update to match backend field name.The change from
HeaderNavModulestoheader_nav_modulesproperly 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
ModuleRoutecomponents. 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 ofsettingresponse type.
cleanUserSettingForResponsenow returnssettingas a JSON‐encoded string; front-end code (e.g. checks onres.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.
There was a problem hiding this comment.
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.
📒 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.
There was a problem hiding this comment.
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 besetShowChangePasswordModal(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.
ReplacesystemSection["enabled"] != truewith 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 defineMAX_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
countdownand 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.
📒 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
getUserDataand 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
ModuleAuthalongsideUserAuth/AdminAuthis 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/selfwill 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.
There was a problem hiding this comment.
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.UpdateTxonly 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.Columnis used on Line 172 butgorm.io/gorm/clauseisn’t imported, causing a compile error. Also addunicode/utf8for 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.
📒 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.
There was a problem hiding this comment.
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.goExpected: 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.
📒 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 unuseduserSettingparameter
userSettingisn’t referenced incalculateFinalSidebarConfig; remove it from the signature and update its sole call at controller/user.go:528 tocalculateFinalSidebarConfig(userRole).
There was a problem hiding this comment.
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
TheUpdateTxmethod in model/user_group.go (lines 40–43) currently callstx.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. useUpdates(map[string]any{"name": g.Name, "description": g.Description, "ratio": g.Ratio})or GORM’sSelect(...)).
♻️ 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 errorsDefaulting 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-safeAccessing 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 eventsCurrent 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 precisionMatch 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) / 1e4Add imports if missing:
- "fmt" + "fmt" + "math" + "unicode/utf8"
105-116: Mirror validation on updateApply 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 UPDATEUse 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 sectionsSections 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: falseflag 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_modulesincludes anenabledflag 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: Replacemin-h-fullwithmin-h-screenon the root container
No ancestor inweb/src/App.jsxorweb/src/components/layoutprovidesh-full, somin-h-fullwon’t guarantee full-viewport height. Update the<div>on line 152 ofweb/src/pages/Home/index.jsxto usemin-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 === falsewith 200),res.statuswon’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 updatesUpcoming 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 transactionFail 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 noiseDrop 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 overheadYou 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.
📒 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.jsweb/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; theInputcomponent’s prop isreadonly(lowercase)
Semi UI’sInputPropsinterface declares areadonly?: booleanprop, so replacing it withreadOnlywould 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 LGTMBoolean and nested object.enabled are correctly handled.
controller/user_group.go (2)
147-151: Transaction handling LGTMRollback paths and commit error handling are correct.
Also applies to: 182-187
273-346: Sync helpers: thread-safe copies LGTMCopy/update flows respect in-memory caches and DB options.
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
39-47: Hooks order fix LGTMState/hooks are now declared before early returns.
There was a problem hiding this comment.
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.
📒 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.
There was a problem hiding this comment.
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.
📒 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
creamlike1024
left a comment
There was a problem hiding this comment.
感谢贡献
有两个小建议,可以参考一下:
- 多个功能建议拆分成多个 PR(比如一个 PR 专注一个 feature/fix),这样每个改动可以单独讨论、测试和快速合并 ✅
- 在推送前,可以用
git rebase -i整理一下 commit 历史,让提交信息更清晰、连贯 🧹
|
如果不好修改可以先将分组管理以外的功能单独提 PR 进行合并 |
|
已经说过了,这个pr改的东西太多,代码提交修改太多,只能拆分开提交新的pr,这个pr是不可能再被接收的了 |
边栏设置设置后界面不能立即生效的问题Summary by CodeRabbit
New Features
UI/UX
i18n
Other