feat(auth,ui): hardened Agnet auth, admin workspace and role whitelist
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
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
1.0.2-agnet-jit
|
||||
1.1.0-default-user-role
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Vendored
+1
-1
@@ -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",
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "newapi-web",
|
||||
"name": "heicode-web",
|
||||
"private": false,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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])
|
||||
|
||||
+81
-6
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
+8
-3
@@ -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<T>(
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 selfRes = (await getSelf()) as {
|
||||
success?: boolean
|
||||
data?: {
|
||||
id?: number
|
||||
username?: string
|
||||
display_name?: string
|
||||
email?: string
|
||||
role?: number
|
||||
status?: number
|
||||
group?: string
|
||||
} | null
|
||||
}
|
||||
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<string, unknown>).language as
|
||||
| string
|
||||
| undefined
|
||||
const savedLang = (heicodeUser as Record<string, unknown>)
|
||||
.language as string | undefined
|
||||
if (savedLang && savedLang !== i18n.language) {
|
||||
i18n.changeLanguage(savedLang)
|
||||
}
|
||||
} else {
|
||||
throw new Error('External auth session invalid')
|
||||
userSet = true
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to fetch user data:', error)
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
|
||||
if (!userSet) {
|
||||
// Use login response as temporary session profile so route guards can pass.
|
||||
auth.setUser(buildFallbackUser(userData))
|
||||
}
|
||||
|
||||
+74
-19
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "次のページ",
|
||||
|
||||
@@ -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": "Следующая страница",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "下一页",
|
||||
|
||||
Vendored
+17
-3
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user