diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d518bc8..6f21ae0 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -22,8 +22,6 @@ declare module 'vue' { ElSelect: typeof import('element-plus/es')['ElSelect'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] - ElTabPane: typeof import('element-plus/es')['ElTabPane'] - ElTabs: typeof import('element-plus/es')['ElTabs'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SubsystemCard: typeof import('./src/components/SubsystemCard.vue')['default'] diff --git a/frontend/src/api/subsystemAuth.ts b/frontend/src/api/subsystemAuth.ts new file mode 100644 index 0000000..bda2ba6 --- /dev/null +++ b/frontend/src/api/subsystemAuth.ts @@ -0,0 +1,143 @@ +import { getToken } from '@/utils/auth' + +export interface SubsystemAuthSystem { + key: string + name: string + enabled: boolean +} + +export interface WayneRole { + id: number + name: string + comment?: string + type: number +} + +export interface WaynePermission { + create: boolean + update: boolean + delete: boolean +} + +export interface WayneBusinessLineNamespace { + id: number + name: string + kubeNamespace: string + permissions?: WaynePermission + can_bind?: boolean + can_unbind?: boolean + permission_error?: string +} + +export interface WayneRoleBindingPayload { + groupIds?: number[] + replace?: boolean + requestId?: string + reason?: string + dryRun?: boolean +} + +export interface WayneUserRoles { + userId?: number + userName?: string + namespaces?: Array<{ + namespace?: { + id: number + name: string + } + groups?: Array<{ + id: number + name: string + }> + }> + apps?: unknown[] +} + +export const subsystemAuthApi = { + async listSystems(): Promise { + const data = await authRequest('/auth/api/v1/subsystem-auth/systems') + return Array.isArray(data.items) ? data.items : [] + }, + + async listWayneRoles(): Promise { + const data = await authRequest('/auth/api/v1/subsystem-auth/wayne/roles') + return Array.isArray(data.items) ? data.items : [] + }, + + async listWayneNamespaces(businessLineId: number): Promise { + const data = await authRequest(`/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/namespaces`) + return Array.isArray(data.items) ? data.items : [] + }, + + async getWayneUserRoles(username: string): Promise { + const data = await authRequest(`/auth/api/v1/subsystem-auth/wayne/users/${encodeURIComponent(username)}/roles`) + return data.data || data + }, + + async bindWayneNamespaceRoles( + businessLineId: number, + namespaceId: number, + username: string, + payload: WayneRoleBindingPayload, + ): Promise { + return authRequest( + `/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/namespaces/${namespaceId}/users/${encodeURIComponent(username)}/roles`, + { + method: 'PUT', + body: JSON.stringify(payload), + }, + ) + }, + + async unbindWayneNamespaceRoles( + businessLineId: number, + namespaceId: number, + username: string, + payload: WayneRoleBindingPayload = {}, + ): Promise { + return authRequest( + `/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/namespaces/${namespaceId}/users/${encodeURIComponent(username)}/roles`, + { + method: 'DELETE', + body: JSON.stringify(payload), + }, + ) + }, + + async initWayneBusinessLineUser(businessLineId: number, userId: number): Promise { + return authRequest(`/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/users/${userId}/init`, { + method: 'POST', + }) + }, +} + +async function authRequest(path: string, init: RequestInit = {}) { + const token = getToken() + const response = await fetch(path, { + ...init, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...init.headers, + }, + }) + const text = await response.text() + const data = parseResponseBody(text) + if (!response.ok) { + const message = data?.error || data?.message || text || `HTTP ${response.status}` + throw new Error(message) + } + return data || {} +} + +function parseResponseBody(text: string) { + if (!text.trim()) { + return {} + } + try { + return JSON.parse(text) + } catch { + return { error: text } + } +} diff --git a/frontend/src/components/Layout/AppSidebar.vue b/frontend/src/components/Layout/AppSidebar.vue index 295871e..a820f0e 100644 --- a/frontend/src/components/Layout/AppSidebar.vue +++ b/frontend/src/components/Layout/AppSidebar.vue @@ -97,7 +97,7 @@ const route = useRoute() const authStore = useAuthStore() const businessLineStore = useBusinessLineStore() const portalExpanded = ref(true) -const isPlatformAdmin = computed(() => authStore.user?.is_admin === true) +const isPlatformAdmin = computed(() => authStore.isAdmin) const isBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin) const subsystems = [ diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index cd68e3a..ad844b4 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -1,11 +1,12 @@ import { defineStore } from 'pinia' -import { ref } from 'vue' +import { computed, ref } from 'vue' import { authApi } from '@/api/auth' import { getToken, setToken, removeToken, getUser, setUser, removeUser } from '@/utils/auth' export const useAuthStore = defineStore('auth', () => { const token = ref(getToken()) const user = ref(getUser()) + const isAdmin = computed(() => user.value?.is_admin === true || decodeAdminClaim(token.value)) function setAuth(newToken: string, newUser: any) { token.value = newToken @@ -45,6 +46,7 @@ export const useAuthStore = defineStore('auth', () => { return { token, user, + isAdmin, setAuth, setSessionToken, refreshUser, @@ -52,3 +54,19 @@ export const useAuthStore = defineStore('auth', () => { isLoggedIn, } }) + +function decodeAdminClaim(token: string | null): boolean { + if (!token) return false + try { + const payload = JSON.parse(decodeBase64Url(token.split('.')[1] || '')) + return payload.admin === true || payload.is_admin === true + } catch { + return false + } +} + +function decodeBase64Url(value: string): string { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/') + const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=') + return atob(padded) +} diff --git a/frontend/src/utils/businessLineMock.ts b/frontend/src/utils/businessLineMock.ts new file mode 100644 index 0000000..95d0e1c --- /dev/null +++ b/frontend/src/utils/businessLineMock.ts @@ -0,0 +1,113 @@ +import { computed } from 'vue' +import { useBusinessLineStore } from '@/stores/businessLine' + +export const BUSINESS_LINE_NAMES = ['kodo', 'linxi', 'xinfra', 'las'] as const +export type BusinessLineName = (typeof BUSINESS_LINE_NAMES)[number] + +interface BusinessLineMockProfile { + name: BusinessLineName + clusters: number + nodes: number + physicalMachines: number + virtualMachines: number + cpuAllocated: number + components: number + mysql: number + redis: number + alertsP0: number + alertsP1: number + tasksRunning: number + primaryZone: string + secondaryZone: string + servicePrefix: string +} + +const profiles: Record = { + kodo: { + name: 'kodo', + clusters: 3, + nodes: 128, + physicalMachines: 186, + virtualMachines: 512, + cpuAllocated: 61, + components: 214, + mysql: 38, + redis: 92, + alertsP0: 1, + alertsP1: 5, + tasksRunning: 2, + primaryZone: 'IDC-华北机房', + secondaryZone: 'IDC-华东机房', + servicePrefix: 'kodo', + }, + linxi: { + name: 'linxi', + clusters: 2, + nodes: 74, + physicalMachines: 96, + virtualMachines: 238, + cpuAllocated: 47, + components: 128, + mysql: 21, + redis: 46, + alertsP0: 0, + alertsP1: 2, + tasksRunning: 1, + primaryZone: 'IDC-华东机房', + secondaryZone: '阿里云-华南', + servicePrefix: 'linxi', + }, + xinfra: { + name: 'xinfra', + clusters: 2, + nodes: 52, + physicalMachines: 68, + virtualMachines: 156, + cpuAllocated: 39, + components: 84, + mysql: 12, + redis: 31, + alertsP0: 0, + alertsP1: 1, + tasksRunning: 1, + primaryZone: 'IDC-华北机房', + secondaryZone: '香港 IDC', + servicePrefix: 'xinfra', + }, + las: { + name: 'las', + clusters: 1, + nodes: 34, + physicalMachines: 42, + virtualMachines: 118, + cpuAllocated: 31, + components: 66, + mysql: 8, + redis: 18, + alertsP0: 0, + alertsP1: 3, + tasksRunning: 0, + primaryZone: '七牛-新加坡', + secondaryZone: 'AWS-美国', + servicePrefix: 'las', + }, +} + +export function normalizeBusinessLineName(name?: string | null): BusinessLineName { + const normalized = String(name || '').trim().toLowerCase() + if (BUSINESS_LINE_NAMES.includes(normalized as BusinessLineName)) { + return normalized as BusinessLineName + } + return 'kodo' +} + +export function useBusinessLineMockProfile() { + const businessLineStore = useBusinessLineStore() + const currentName = computed(() => normalizeBusinessLineName(businessLineStore.current?.name)) + const profile = computed(() => profiles[currentName.value]) + + return { + currentName, + profile, + } +} diff --git a/frontend/src/views/cluster/ClusterList.vue b/frontend/src/views/cluster/ClusterList.vue index 80b0536..7f945fe 100644 --- a/frontend/src/views/cluster/ClusterList.vue +++ b/frontend/src/views/cluster/ClusterList.vue @@ -24,7 +24,7 @@ - + {{ cluster.name }} {{ cluster.zone }} ● {{ cluster.status }} @@ -43,7 +43,7 @@
-

rke2-bj-prod-01 · 节点列表(节选)

+

{{ businessLineClusters[0]?.name }} · 节点列表(节选)

node-label 多租户隔离
@@ -60,7 +60,7 @@ - + {{ node.name }} {{ node.ip }} {{ node.label }} @@ -77,7 +77,10 @@ diff --git a/frontend/src/views/task/TaskCenter.vue b/frontend/src/views/task/TaskCenter.vue index dcd1882..80296e0 100644 --- a/frontend/src/views/task/TaskCenter.vue +++ b/frontend/src/views/task/TaskCenter.vue @@ -15,7 +15,7 @@
- + @@ -23,7 +23,7 @@
● {{ task.status }} {{ task.name }} {{ task.playbook }}