From a668e1fca1286abde6c99d10c0b7aaa99b864865 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 30 Apr 2026 21:03:08 +0000 Subject: [PATCH] feat(auth,ui): hardened Agnet auth, admin workspace and role whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (controller/heicode_agnet_session.go): - Add HEICODE_ROOT_EMAILS / HEICODE_ADMIN_EMAILS whitelists for JIT role assignment. Manager no longer trusts Agnet's role claim — admin / root is granted only by local config. - Default JIT-synced users to RoleCommonUser. - Promote-only role sync on every login (never demote). Frontend auth fixes: - login() no longer hard-codes id=1; preserves the real manager user id returned by /api/user/session/from-agnet so the New-Api-User header matches the cookie session. - After login, prefer local /api/user/self over Agnet /me so role / status reflect actual manager state (e.g. whitelist promotion). - lib/api.ts: scope 401 -> "Session expired" handling to identity endpoints only; admin-only 401 no longer resets the session. UI restructuring: - Default sidebar shows only Code delivery + Console + Personal, plus a single "System settings" entry for ROLE.ADMIN+. - system-settings workspace now hosts the full Tenant administration tree (Channels / Models / Subscriptions / Redemption codes / Tenants / Templates / Agents / Vendors / All usage logs) for ROLE.ADMIN+, with System Administration sub-tree gated to ROLE.SUPER_ADMIN. - Workspace switch triggers on admin paths (channels, users, templates, agents, subscriptions, models, redemption-codes) — not only /system-settings. - system-settings route now allows ROLE.ADMIN+ instead of root-only. Branding cleanup: - Drop orphan "NewAPI" i18n keys from web/default locales. - Rename web/default workspace package newapi-web -> heicode-web. Config: - docker-compose.azure-vm.yml exposes HEICODE_ROOT_EMAILS / HEICODE_ADMIN_EMAILS. VERSION: 1.1.0-default-user-role Made-with: Cursor --- heicode/VERSION | 2 +- heicode/controller/heicode_agnet_session.go | 50 ++++++++-- heicode/docker-compose.azure-vm.yml | 3 + heicode/web/default/bun.lock | 2 +- heicode/web/default/package.json | 2 +- .../layout/components/app-sidebar.tsx | 13 ++- .../layout/config/system-settings.config.ts | 87 +++++++++++++++-- .../layout/lib/workspace-registry.ts | 5 +- heicode/web/default/src/features/auth/api.ts | 11 ++- .../features/auth/hooks/use-auth-redirect.ts | 61 ++++++++---- .../web/default/src/hooks/use-sidebar-data.ts | 93 +++++++++++++++---- .../locales/_reports/ja.untranslated.json | 1 - .../locales/_reports/ru.untranslated.json | 1 - .../locales/_reports/zh.untranslated.json | 1 - heicode/web/default/src/i18n/locales/en.json | 1 - heicode/web/default/src/i18n/locales/fr.json | 1 - heicode/web/default/src/i18n/locales/ja.json | 1 - heicode/web/default/src/i18n/locales/ru.json | 1 - heicode/web/default/src/i18n/locales/vi.json | 1 - heicode/web/default/src/i18n/locales/zh.json | 1 - heicode/web/default/src/lib/api.ts | 20 +++- .../_authenticated/system-settings/route.tsx | 5 +- 22 files changed, 289 insertions(+), 74 deletions(-) diff --git a/heicode/VERSION b/heicode/VERSION index 0aac1c0..8b352bc 100644 --- a/heicode/VERSION +++ b/heicode/VERSION @@ -1 +1 @@ -1.0.2-agnet-jit \ No newline at end of file +1.1.0-default-user-role diff --git a/heicode/controller/heicode_agnet_session.go b/heicode/controller/heicode_agnet_session.go index dae4e2a..7f29ab6 100644 --- a/heicode/controller/heicode_agnet_session.go +++ b/heicode/controller/heicode_agnet_session.go @@ -69,15 +69,41 @@ func jitUsernameFromEmail(email string) string { return "ag_" + hex.EncodeToString(sum[:])[:16] } -func roleFromAgnet(role string) int { - switch strings.ToLower(strings.TrimSpace(role)) { - case "root": - return common.RoleRootUser - case "admin": - return common.RoleAdminUser - default: - return common.RoleCommonUser +// parseEmailList returns a set of normalized lowercase emails from an env value. +func parseEmailList(raw string) map[string]struct{} { + out := map[string]struct{}{} + for _, e := range strings.Split(raw, ",") { + e = strings.ToLower(strings.TrimSpace(e)) + if e != "" { + out[e] = struct{}{} + } } + return out +} + +// roleFromAgnetWithEmail decides the local role for a JIT-synced Agnet user. +// +// 安全策略:管理员权限只能通过本地配置(环境变量白名单)显式授予, +// **不信任** Agnet 平台返回的 role 字段。这样防止外部身份平台 +// 的角色被直接映射到 Manager 的高权限角色。 +// +// - 邮箱命中 HEICODE_ROOT_EMAILS -> RoleRootUser +// - 邮箱命中 HEICODE_ADMIN_EMAILS -> RoleAdminUser +// - 其他任何情况 -> RoleCommonUser(默认普通用户) +// +// 第二参数 `role` 当前未使用,保留是为了未来扩展(例如在策略中允许 +// 信任部分上游 role),不破坏调用点签名。 +func roleFromAgnetWithEmail(_ string, email string) int { + emailKey := strings.ToLower(strings.TrimSpace(email)) + rootEmails := parseEmailList(os.Getenv("HEICODE_ROOT_EMAILS")) + if _, ok := rootEmails[emailKey]; ok { + return common.RoleRootUser + } + adminEmails := parseEmailList(os.Getenv("HEICODE_ADMIN_EMAILS")) + if _, ok := adminEmails[emailKey]; ok { + return common.RoleAdminUser + } + return common.RoleCommonUser } func statusFromAgnet(status string) int { @@ -170,7 +196,7 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) { Password: common.GetRandomString(32), DisplayName: display, Email: email, - Role: roleFromAgnet(me.Data.Role), + Role: roleFromAgnetWithEmail(me.Data.Role, email), Status: statusFromAgnet(me.Data.Status), Group: group, } @@ -204,6 +230,12 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) { user.Group = ch changed = true } + // Promote role from Agnet / email whitelist on every login (never demote). + desiredRole := roleFromAgnetWithEmail(me.Data.Role, email) + if desiredRole > user.Role { + user.Role = desiredRole + changed = true + } if changed { if err := user.Update(false); err != nil { return nil, err diff --git a/heicode/docker-compose.azure-vm.yml b/heicode/docker-compose.azure-vm.yml index 7d0fdb6..b7540b3 100644 --- a/heicode/docker-compose.azure-vm.yml +++ b/heicode/docker-compose.azure-vm.yml @@ -44,6 +44,9 @@ services: - NODE_NAME=heicode-node-1 # 默认与 docs/integration/Heicode-登录接口对接文档.md §2.1 一致;覆盖仅用于非标准网关。 - HEICODE_AUTH_BASE_URL=${HEICODE_AUTH_BASE_URL:-https://apimtaiji.azure-api.net/api/mcp} + # Agnet 登录后 JIT 同步:邮箱命中以下白名单则自动提权 + - HEICODE_ROOT_EMAILS=${HEICODE_ROOT_EMAILS:-} + - HEICODE_ADMIN_EMAILS=${HEICODE_ADMIN_EMAILS:-} networks: - heicode-network healthcheck: diff --git a/heicode/web/default/bun.lock b/heicode/web/default/bun.lock index 071e175..56e0a3f 100644 --- a/heicode/web/default/bun.lock +++ b/heicode/web/default/bun.lock @@ -3,7 +3,7 @@ "configVersion": 0, "workspaces": { "": { - "name": "newapi-web", + "name": "heicode-web", "dependencies": { "@fontsource-variable/public-sans": "^5.2.7", "@hookform/resolvers": "^5.2.2", diff --git a/heicode/web/default/package.json b/heicode/web/default/package.json index 2f77e9d..a9702e5 100644 --- a/heicode/web/default/package.json +++ b/heicode/web/default/package.json @@ -1,5 +1,5 @@ { - "name": "newapi-web", + "name": "heicode-web", "private": false, "version": "1.0.0", "type": "module", diff --git a/heicode/web/default/src/components/layout/components/app-sidebar.tsx b/heicode/web/default/src/components/layout/components/app-sidebar.tsx index 863c1ac..e18617d 100644 --- a/heicode/web/default/src/components/layout/components/app-sidebar.tsx +++ b/heicode/web/default/src/components/layout/components/app-sidebar.tsx @@ -38,13 +38,18 @@ export function AppSidebar() { const configFilteredNavGroups = useSidebarConfig(allNavGroups) // Filter navigation groups based on user role - // Non-Admin users cannot see Admin navigation group + // - admin-entry group: visible only to ROLE.ADMIN+ + // (single entry point that switches into the system-settings workspace) + // - admin group (legacy): visible only to ROLE.ADMIN+ + // - system group (legacy): visible only to ROLE.SUPER_ADMIN (root) const currentNavGroups = useMemo(() => { const isAdmin = userRole && userRole >= ROLE.ADMIN + const isRoot = userRole && userRole >= ROLE.SUPER_ADMIN return configFilteredNavGroups.filter((group) => { - if (group.id === 'admin') { - return isAdmin - } + if (group.id === 'admin-entry') return Boolean(isAdmin) + if (group.id === 'admin') return Boolean(isAdmin) + if (group.id === 'system') return Boolean(isRoot) + if (group.id === 'system-administration') return Boolean(isRoot) return true }) }, [configFilteredNavGroups, userRole]) diff --git a/heicode/web/default/src/components/layout/config/system-settings.config.ts b/heicode/web/default/src/components/layout/config/system-settings.config.ts index acba76e..e44772a 100644 --- a/heicode/web/default/src/components/layout/config/system-settings.config.ts +++ b/heicode/web/default/src/components/layout/config/system-settings.config.ts @@ -1,11 +1,21 @@ import { type TFunction } from 'i18next' import { + Box, + Boxes, + Building2, + CircuitBoard, + ClipboardList, + Cpu, + Layout, + ListChecks, + Plug, + Receipt, Settings, Shield, ShieldAlert, - Layout, - Plug, - Box, + Store, + TerminalSquare, + Ticket, Wrench, } from 'lucide-react' import { getAuthSectionNavItems } from '@/features/system-settings/auth/section-registry.tsx' @@ -18,13 +28,78 @@ import { getRequestLimitsSectionNavItems } from '@/features/system-settings/requ import { type NavGroup } from '../types' /** - * System settings sidebar configuration - * Displayed when switching to "System Settings" workspace + * System Settings workspace 侧边栏(管理员入口后才显示)。 + * + * 包含两大块: + * 1. Tenant administration (ROLE.ADMIN+ 业务管理类入口) + * 2. System Administration (ROLE.SUPER_ADMIN 平台层级配置) + * + * Workspace 进入条件:URL 命中 `/system-settings/...`。 + * 角色过滤由 `app-sidebar.tsx` 按 group.id 完成。 */ export const WORKSPACE_SYSTEM_SETTINGS_ID = 'system-settings' export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { return [ + // ============ Tenant administration (ROLE.ADMIN+) ============ + { + id: 'admin', + title: t('Tenant administration'), + items: [ + { + title: t('Channels'), + url: '/channels', + icon: CircuitBoard, + }, + { + title: t('Models'), + url: '/models', + icon: Boxes, + }, + { + title: t('Model deployments'), + url: '/deployments', + icon: Cpu, + }, + { + title: t('Subscription plans'), + url: '/subscriptions', + icon: Receipt, + }, + { + title: t('Redemption codes'), + url: '/redemption-codes', + icon: Ticket, + }, + { + title: t('Tenants'), + url: '/users', + icon: Building2, + }, + { + title: t('Templates'), + url: '/templates', + icon: ListChecks, + }, + { + title: t('Agents'), + url: '/agents', + icon: TerminalSquare, + }, + { + title: t('Vendors'), + url: '/system-settings/integrations', + icon: Store, + }, + { + title: t('All usage logs'), + url: '/usage-logs', + icon: ClipboardList, + }, + ], + }, + + // ============ System Administration (ROLE.SUPER_ADMIN) ============ { id: 'system-administration', title: t('System Administration'), @@ -55,7 +130,7 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] { items: getIntegrationsSectionNavItems(t), }, { - title: t('Models'), + title: t('Models catalog'), icon: Box, items: getModelsSectionNavItems(t), }, diff --git a/heicode/web/default/src/components/layout/lib/workspace-registry.ts b/heicode/web/default/src/components/layout/lib/workspace-registry.ts index 443337b..394563f 100644 --- a/heicode/web/default/src/components/layout/lib/workspace-registry.ts +++ b/heicode/web/default/src/components/layout/lib/workspace-registry.ts @@ -43,10 +43,13 @@ export type WorkspaceConfig = { */ const workspaceRegistry: WorkspaceConfig[] = [ // System Settings workspace + // 触发条件:访问 /system-settings/* 或任意 admin 管理类路径(频道/模型/兑换码/ + // 租户/模板/Agents/订阅管理/Models 管理 等)。 { id: WORKSPACE_IDS.SYSTEM_SETTINGS, name: 'System Settings', - pathPattern: /^\/system-settings/, + pathPattern: + /^\/(system-settings|channels|redemption-codes|users|templates|agents|subscriptions|models)(\/|$)/, getNavGroups: getSystemSettingsNavGroups, }, // Default workspace (must be last) diff --git a/heicode/web/default/src/features/auth/api.ts b/heicode/web/default/src/features/auth/api.ts index 15028cd..aebbef0 100644 --- a/heicode/web/default/src/features/auth/api.ts +++ b/heicode/web/default/src/features/auth/api.ts @@ -53,7 +53,9 @@ export function clearHeicodeTokens() { * 外部 Agnet 登录成功后,用 token 向本站校验身份并写入 Manager 会话 Cookie。 * 不在本站再做密码校验;本地用户按需 JIT 创建。 */ -async function establishManagerSessionFromAgnet() { +async function establishManagerSessionFromAgnet(): Promise<{ + managerUserId?: number +}> { const access_token = readToken(ACCESS_TOKEN_KEY) const refresh_token = readToken(REFRESH_TOKEN_KEY) if (!access_token) { @@ -95,6 +97,7 @@ async function establishManagerSessionFromAgnet() { if (body.data?.id != null) { saveUserId(body.data.id) } + return { managerUserId: body.data?.id } } async function callHeicodeAuth( @@ -164,10 +167,12 @@ export async function login(payload: LoginPayload) { role: 'user', }), }) + let managerUserId: number | undefined if (res?.success) { writeTokens(res.data?.token, res.data?.refreshToken) try { - await establishManagerSessionFromAgnet() + const sessionRes = await establishManagerSessionFromAgnet() + managerUserId = sessionRes.managerUserId } catch (syncErr) { if (isTwoFactorRequiredError(syncErr)) { throw syncErr @@ -182,7 +187,7 @@ export async function login(payload: LoginPayload) { success: Boolean(res?.success), message: res?.message || res?.detail || '', data: { - id: 1, + id: managerUserId, user: res?.data?.user, }, } diff --git a/heicode/web/default/src/features/auth/hooks/use-auth-redirect.ts b/heicode/web/default/src/features/auth/hooks/use-auth-redirect.ts index 821b8eb..6398afc 100644 --- a/heicode/web/default/src/features/auth/hooks/use-auth-redirect.ts +++ b/heicode/web/default/src/features/auth/hooks/use-auth-redirect.ts @@ -2,6 +2,7 @@ import { useNavigate } from '@tanstack/react-router' import i18n from 'i18next' import { useAuthStore } from '@/stores/auth-store' import { getHeicodeCurrentUser } from '@/features/auth/api' +import { getSelf } from '@/lib/api' import { markHeicodeAuthenticatedSessionVerified } from '@/features/auth/heicode-authenticated-session' import { saveUserId } from '../lib/storage' @@ -75,24 +76,52 @@ export function useAuthRedirect() { saveUserId(userData.id) } - // Fetch and set user data from external auth only + // 优先从本地 Manager 拉真实用户(含 role / status / quota), + // 因为 Agnet 上的 role 不一定与本地 JIT/管理员白名单同步后的角色一致。 + let userSet = false try { - const heicodeUser = await getHeicodeCurrentUser() - if (heicodeUser) { - auth.setUser(heicodeUser) - saveUserId(heicodeUser.id) - const savedLang = (heicodeUser as Record).language as - | string - | undefined - if (savedLang && savedLang !== i18n.language) { - i18n.changeLanguage(savedLang) - } - } else { - throw new Error('External auth session invalid') + const selfRes = (await getSelf()) as { + success?: boolean + data?: { + id?: number + username?: string + display_name?: string + email?: string + role?: number + status?: number + group?: string + } | null } - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to fetch user data:', error) + if (selfRes?.success && selfRes.data) { + auth.setUser(selfRes.data as never) + if (selfRes.data.id != null) { + saveUserId(selfRes.data.id) + } + userSet = true + } + } catch { + // Fall through to Agnet /me / fallback below. + } + + if (!userSet) { + try { + const heicodeUser = await getHeicodeCurrentUser() + if (heicodeUser) { + auth.setUser(heicodeUser) + saveUserId(heicodeUser.id) + const savedLang = (heicodeUser as Record) + .language as string | undefined + if (savedLang && savedLang !== i18n.language) { + i18n.changeLanguage(savedLang) + } + userSet = true + } + } catch { + /* fall through */ + } + } + + if (!userSet) { // Use login response as temporary session profile so route guards can pass. auth.setUser(buildFallbackUser(userData)) } diff --git a/heicode/web/default/src/hooks/use-sidebar-data.ts b/heicode/web/default/src/hooks/use-sidebar-data.ts index 1aa8024..aa60bc7 100644 --- a/heicode/web/default/src/hooks/use-sidebar-data.ts +++ b/heicode/web/default/src/hooks/use-sidebar-data.ts @@ -1,30 +1,35 @@ import { Activity, BookOpenText, - Building2, Command, + FileBarChart, + KeyRound, LayoutDashboard, + PlaySquare, Rocket, Settings, ShieldCheck, + UserCog, + Wallet, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { WORKSPACE_IDS } from '@/components/layout/lib/workspace-registry' import { type SidebarData } from '@/components/layout/types' /** - * Heicode Manager sidebar IA — code delivery command axis. + * Heicode Manager 默认 workspace 侧边栏。 * - * Main axis (always visible to authenticated users): - * Overview / Deployments / Events / SK Snapshots / Audit + * 设计原则: + * - 默认进来界面干净,仅展示 Code delivery cockpit + 个人/控制台入口; + * - 所有 admin/root 管理类入口(Channels/Models/Tenants/Redemption/...) + * 收纳进 "系统设置" workspace(点击进入 /system-settings 后才出现)。 * - * Admin axis (RoleAdminUser+): - * Tenants / Settings + * Visibility rules (filter happens in `app-sidebar.tsx` by `group.id`): + * - everyone (no id filter): cockpit / console / personal + * - id === 'admin-entry' -> ROLE.ADMIN+ (单一入口:系统设置) * - * Gateway-flavored entries (Provider Channels, Model Catalog, - * Subscriptions, API Keys, Playground, Redemption Codes...) are - * intentionally NOT on the main axis. They live as sub-pages under - * Settings or are deprecated outright. + * 注意:进入 /system-settings 之后侧边栏由 + * `components/layout/config/system-settings.config.ts` 接管。 */ export function useSidebarData(): SidebarData { const { t } = useTranslation() @@ -39,6 +44,7 @@ export function useSidebarData(): SidebarData { }, ], navGroups: [ + // ============ Code delivery cockpit ============ { id: 'cockpit', title: t('Code delivery'), @@ -66,24 +72,73 @@ export function useSidebarData(): SidebarData { { title: t('Audit'), url: '/audit', - activeUrls: ['/usage-logs', '/usage-logs/common'], icon: ShieldCheck, }, ], }, + + // ============ Console (登录用户均可见) ============ { - id: 'admin', - title: t('Tenant administration'), + id: 'console', + title: t('Console'), items: [ { - title: t('Tenants'), - url: '/users', - icon: Building2, + title: t('Playground'), + url: '/playground', + icon: PlaySquare, }, { - title: t('Settings'), - url: '/system-settings/general', - activeUrls: ['/system-settings'], + title: t('API Keys'), + url: '/keys', + icon: KeyRound, + }, + { + title: t('Usage logs'), + url: '/usage-logs', + icon: FileBarChart, + }, + ], + }, + + // ============ Personal ============ + { + id: 'personal', + title: t('Personal'), + items: [ + { + title: t('Wallet'), + url: '/wallet', + icon: Wallet, + }, + { + title: t('Profile'), + url: '/profile', + icon: UserCog, + }, + ], + }, + + // ============ Admin entry (仅 ROLE.ADMIN+ 可见) ============ + // 单一入口:跳转到管理 workspace。admin 进入 /channels(管理首页), + // root 进入 /system-settings/general。两种角色进入后侧边栏均切换为 + // system-settings workspace(由 workspace-registry 路径匹配触发)。 + { + id: 'admin-entry', + title: t('Administration'), + items: [ + { + title: t('System settings'), + url: '/channels', + activeUrls: [ + '/system-settings', + '/channels', + '/users', + '/redemption-codes', + '/templates', + '/agents', + '/subscriptions', + '/models', + ], icon: Settings, }, ], diff --git a/heicode/web/default/src/i18n/locales/_reports/ja.untranslated.json b/heicode/web/default/src/i18n/locales/_reports/ja.untranslated.json index 260cb2f..dfcbfc1 100644 --- a/heicode/web/default/src/i18n/locales/_reports/ja.untranslated.json +++ b/heicode/web/default/src/i18n/locales/_reports/ja.untranslated.json @@ -56,7 +56,6 @@ "Moonshot": "Moonshot", "my-status": "my-status", "name@example.com": "name@example.com", - "NewAPI": "NewAPI", "noreply@example.com": "noreply@example.com", "OhMyGPT": "OhMyGPT", "Ollama": "Ollama", diff --git a/heicode/web/default/src/i18n/locales/_reports/ru.untranslated.json b/heicode/web/default/src/i18n/locales/_reports/ru.untranslated.json index 2a6c3ae..c715614 100644 --- a/heicode/web/default/src/i18n/locales/_reports/ru.untranslated.json +++ b/heicode/web/default/src/i18n/locales/_reports/ru.untranslated.json @@ -59,7 +59,6 @@ "MokaAI": "MokaAI", "Moonshot": "Moonshot", "name@example.com": "name@example.com", - "NewAPI": "NewAPI", "noreply@example.com": "noreply@example.com", "OAuth Client Secret": "OAuth Client Secret", "OhMyGPT": "OhMyGPT", diff --git a/heicode/web/default/src/i18n/locales/_reports/zh.untranslated.json b/heicode/web/default/src/i18n/locales/_reports/zh.untranslated.json index 93ac9d6..f14432a 100644 --- a/heicode/web/default/src/i18n/locales/_reports/zh.untranslated.json +++ b/heicode/web/default/src/i18n/locales/_reports/zh.untranslated.json @@ -65,7 +65,6 @@ "name@example.com": "name@example.com", "New API": "New API", "New API <noreply@example.com>": "New API <noreply@example.com>", - "NewAPI": "NewAPI", "noreply@example.com": "noreply@example.com", "OhMyGPT": "OhMyGPT", "Ollama": "Ollama", diff --git a/heicode/web/default/src/i18n/locales/en.json b/heicode/web/default/src/i18n/locales/en.json index f275eb1..b4705eb 100644 --- a/heicode/web/default/src/i18n/locales/en.json +++ b/heicode/web/default/src/i18n/locales/en.json @@ -2074,7 +2074,6 @@ "New password must be different from current password": "New password must be different from current password", "New User Quota": "New User Quota", "New version available: {{version}}": "New version available: {{version}}", - "NewAPI": "NewAPI", "Next": "Next", "Next branch": "Next branch", "Next page": "Next page", diff --git a/heicode/web/default/src/i18n/locales/fr.json b/heicode/web/default/src/i18n/locales/fr.json index 0bf9621..a006857 100644 --- a/heicode/web/default/src/i18n/locales/fr.json +++ b/heicode/web/default/src/i18n/locales/fr.json @@ -2074,7 +2074,6 @@ "New password must be different from current password": "Le nouveau mot de passe doit être différent de l'actuel", "New User Quota": "Quota nouvel utilisateur", "New version available: {{version}}": "Nouvelle version disponible : {{version}}", - "NewAPI": "NewAPI", "Next": "Suivant", "Next branch": "Branche suivante", "Next page": "Page suivante", diff --git a/heicode/web/default/src/i18n/locales/ja.json b/heicode/web/default/src/i18n/locales/ja.json index a06def5..034fc00 100644 --- a/heicode/web/default/src/i18n/locales/ja.json +++ b/heicode/web/default/src/i18n/locales/ja.json @@ -2074,7 +2074,6 @@ "New password must be different from current password": "新しいパスワードは現在のものと異なっていなければなりません", "New User Quota": "新しいユーザー割り当て", "New version available: {{version}}": "新しいバージョンが利用可能です:{{version}}", - "NewAPI": "NewAPI", "Next": "次へ", "Next branch": "次のブランチ", "Next page": "次のページ", diff --git a/heicode/web/default/src/i18n/locales/ru.json b/heicode/web/default/src/i18n/locales/ru.json index 90c11c0..cc1ad35 100644 --- a/heicode/web/default/src/i18n/locales/ru.json +++ b/heicode/web/default/src/i18n/locales/ru.json @@ -2074,7 +2074,6 @@ "New password must be different from current password": "Новый пароль должен отличаться от текущего пароля", "New User Quota": "Новая квота пользователя", "New version available: {{version}}": "Доступна новая версия: {{version}}", - "NewAPI": "NewAPI", "Next": "Следующий шаг", "Next branch": "Следующая ветка", "Next page": "Следующая страница", diff --git a/heicode/web/default/src/i18n/locales/vi.json b/heicode/web/default/src/i18n/locales/vi.json index b59fd86..4875d76 100644 --- a/heicode/web/default/src/i18n/locales/vi.json +++ b/heicode/web/default/src/i18n/locales/vi.json @@ -2074,7 +2074,6 @@ "New password must be different from current password": "Mật khẩu mới phải khác với mật khẩu hiện tại", "New User Quota": "Hạn mức người dùng mới", "New version available: {{version}}": "Có phiên bản mới: {{version}}", - "NewAPI": "NewAPI", "Next": "Tiếp", "Next branch": "Nhánh tiếp theo", "Next page": "Trang tiếp theo", diff --git a/heicode/web/default/src/i18n/locales/zh.json b/heicode/web/default/src/i18n/locales/zh.json index 29e3125..3e5ae0c 100644 --- a/heicode/web/default/src/i18n/locales/zh.json +++ b/heicode/web/default/src/i18n/locales/zh.json @@ -2074,7 +2074,6 @@ "New password must be different from current password": "新密码必须与当前密码不同", "New User Quota": "新用户配额", "New version available: {{version}}": "有新版本可用:{{version}}", - "NewAPI": "NewAPI", "Next": "下一步", "Next branch": "下一个分支", "Next page": "下一页", diff --git a/heicode/web/default/src/lib/api.ts b/heicode/web/default/src/lib/api.ts index 3abb994..424270b 100644 --- a/heicode/web/default/src/lib/api.ts +++ b/heicode/web/default/src/lib/api.ts @@ -76,17 +76,31 @@ api.interceptors.response.use( const skip = error?.config?.skipErrorHandler if (!skip) { const status = error?.response?.status + const url = String(error?.config?.url || '') - if (status === 401) { - // Unauthorized: clear auth state and show toast + // Only treat 401 on identity/self endpoints as a real session expiry. + // Admin-only endpoints (/api/agnet, /api/channel, etc.) returning 401 for + // non-admin users should NOT reset the session. + const isIdentityEndpoint = + url.includes('/api/user/self') || + url.includes('/api/user/logout') || + url.endsWith('/api/user/login') + + if (status === 401 && isIdentityEndpoint) { toast.error(i18next.t('Session expired!')) try { useAuthStore.getState().auth.reset() } catch { /* empty */ } + } else if (status === 401) { + // Insufficient permission for this specific request — surface the + // backend message (if any) but do not kill the session. + const msg = + error?.response?.data?.message || + i18next.t('Insufficient permission') + toast.error(msg) } else { - // Other errors: show error message from response or default const msg = error?.response?.data?.message || error?.message || 'Request error' toast.error(msg) diff --git a/heicode/web/default/src/routes/_authenticated/system-settings/route.tsx b/heicode/web/default/src/routes/_authenticated/system-settings/route.tsx index 8e087f5..dd4a2ef 100644 --- a/heicode/web/default/src/routes/_authenticated/system-settings/route.tsx +++ b/heicode/web/default/src/routes/_authenticated/system-settings/route.tsx @@ -7,7 +7,10 @@ export const Route = createFileRoute('/_authenticated/system-settings')({ beforeLoad: () => { const { auth } = useAuthStore.getState() - if (auth.user?.role !== ROLE.SUPER_ADMIN) { + // 允许 ADMIN+ 进入系统设置 workspace(其中具体子页面会再做权限校验: + // Tenant administration 子页面只要 ADMIN 即可, + // System Administration 子页面要求 SUPER_ADMIN)。 + if (!auth.user || auth.user.role < ROLE.ADMIN) { throw redirect({ to: '/403', })