From d264924d1ee1c315e78408a2ab83620c95538908 Mon Sep 17 00:00:00 2001 From: mac Date: Fri, 17 Jul 2026 15:02:18 +0800 Subject: [PATCH] feat: add business line permissions and Wayne namespace mapping --- frontend/components.d.ts | 8 + frontend/src/api/auth.ts | 2 + frontend/src/api/businessLine.ts | 109 +++++ frontend/src/api/subsystem.ts | 11 +- frontend/src/api/user.ts | 23 + .../src/components/BusinessLineSwitcher.vue | 7 +- frontend/src/components/Layout/AppSidebar.vue | 12 + frontend/src/composables/useAuth.ts | 4 + frontend/src/router/index.ts | 20 + frontend/src/stores/businessLine.ts | 143 +++--- .../src/views/businessLine/Assignment.vue | 236 ++++++++++ frontend/src/views/businessLine/Manage.vue | 204 +++++++++ .../src/views/subsystem/SubsystemDetail.vue | 2 - server/.gitignore | 3 +- server/docs/mysql-init.sql | 47 ++ server/internal/database/database.go | 3 + server/internal/handler/business_line.go | 431 ++++++++++++++++++ server/internal/handler/user.go | 28 +- server/internal/handler/wayen.go | 57 ++- server/internal/model/models.go | 26 ++ server/internal/router/router.go | 12 +- server/internal/service/wayen.go | 8 +- server/internal/service/wayen_test.go | 28 +- 23 files changed, 1343 insertions(+), 81 deletions(-) create mode 100644 frontend/src/api/businessLine.ts create mode 100644 frontend/src/api/user.ts create mode 100644 frontend/src/views/businessLine/Assignment.vue create mode 100644 frontend/src/views/businessLine/Manage.vue create mode 100644 server/docs/mysql-init.sql create mode 100644 server/internal/handler/business_line.go diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 4380d44..6f21ae0 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -13,12 +13,20 @@ declare module 'vue' { AuditLogTable: typeof import('./src/components/AuditLogTable.vue')['default'] BusinessLineSwitcher: typeof import('./src/components/BusinessLineSwitcher.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElOption: typeof import('element-plus/es')['ElOption'] + ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SubsystemCard: typeof import('./src/components/SubsystemCard.vue')['default'] } + export interface ComponentCustomProperties { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } } diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index a9d70a1..deb0621 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -12,6 +12,7 @@ export interface UserInfo { display_name: string email: string business_line: string + is_admin: boolean } export interface LoginResponse { @@ -113,6 +114,7 @@ export const authApi = { display_name: data.display_name || data.username, email: data.email || '', business_line: data.business_line || '', + is_admin: data.is_admin === true, }, } }, diff --git a/frontend/src/api/businessLine.ts b/frontend/src/api/businessLine.ts new file mode 100644 index 0000000..ed0a073 --- /dev/null +++ b/frontend/src/api/businessLine.ts @@ -0,0 +1,109 @@ +import { getToken } from '@/utils/auth' + +export interface BusinessLine { + id: number + name: string + created_at: string + updated_at: string + permission?: 0 | 1 +} + +export interface WayneNamespace { + id: number + name: string + kubeNamespace: string +} + +export const businessLineApi = { + async listMine(): Promise { + const token = getToken() + const response = await fetch('/auth/api/v1/business-lines', { + headers: { + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }) + const data = await response.json().catch(() => ({})) + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`) + } + return Array.isArray(data.items) ? data.items : [] + }, + + async listAll(): Promise { + const data = await request('/auth/api/v1/business-lines/all') + return Array.isArray(data.items) ? data.items : [] + }, + + async create(name: string): Promise { + return request('/auth/api/v1/business-lines', { + method: 'POST', + body: JSON.stringify({ name }), + }) + }, + + async update(id: number, name: string): Promise { + return request(`/auth/api/v1/business-lines/${id}`, { + method: 'PUT', + body: JSON.stringify({ name }), + }) + }, + + async remove(id: number): Promise { + await request(`/auth/api/v1/business-lines/${id}`, { + method: 'DELETE', + }) + }, + + async grant(payload: { + business_line_id: number + target_user_id: number + target_business_line_id: number + permission: 0 | 1 + }): Promise { + await request('/auth/api/v1/business-lines/authorizations', { + method: 'POST', + body: JSON.stringify(payload), + }) + }, + + async listWayneNamespaces(): Promise { + const data = await request('/auth/api/v1/wayne/namespaces') + const items = Array.isArray(data.data) ? data.data : Array.isArray(data.items) ? data.items : [] + return items.map((item: any) => ({ + id: Number(item.id), + name: item.name || '', + kubeNamespace: item.kubeNamespace || item.kube_namespace || '', + })) + }, + + async listMappedWayneNamespaces(businessLineId: number): Promise { + const data = await request(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`) + return Array.isArray(data.items) ? data.items : [] + }, + + async replaceMappedWayneNamespaces(businessLineId: number, namespaces: WayneNamespace[]): Promise { + await request(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`, { + method: 'PUT', + body: JSON.stringify({ namespaces }), + }) + }, +} + +async function request(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 data = await response.json().catch(() => ({})) + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`) + } + return data +} diff --git a/frontend/src/api/subsystem.ts b/frontend/src/api/subsystem.ts index 752b08c..18594b8 100644 --- a/frontend/src/api/subsystem.ts +++ b/frontend/src/api/subsystem.ts @@ -1,6 +1,7 @@ import type { ApiResponse } from '@/types/api' import { getToken, removeToken } from '@/utils/auth' import { redirectToSSO } from '@/utils/sso' +import { useBusinessLineStore } from '@/stores/businessLine' export interface Subsystem { id: number @@ -126,7 +127,7 @@ export const subsystemApi = { const token = getToken() const openApp = subsystem.name === 'CloudDM' ? 'clouddm' : 'wayne' - const path = subsystem.name === 'CloudDM' ? '/auth/api/v1/clouddm/login' : '/auth/api/v1/wayen/login' + const path = subsystem.name === 'CloudDM' ? '/auth/api/v1/clouddm/login' : wayneLoginPath() const response = await fetch(path, { headers: { Accept: 'application/json', @@ -164,3 +165,11 @@ export const subsystemApi = { } }, } + +function wayneLoginPath(): string { + const businessLineID = useBusinessLineStore().current?.id + if (!businessLineID) { + return '/auth/api/v1/wayen/login' + } + return `/auth/api/v1/wayen/login?business_line_id=${encodeURIComponent(String(businessLineID))}` +} diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts new file mode 100644 index 0000000..7f3c85c --- /dev/null +++ b/frontend/src/api/user.ts @@ -0,0 +1,23 @@ +import { getToken } from '@/utils/auth' + +export interface UserOption { + uid: number + username: string +} + +export const userApi = { + async list(): Promise { + const token = getToken() + const response = await fetch('/auth/api/v1/users', { + headers: { + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }) + const data = await response.json().catch(() => ({})) + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`) + } + return Array.isArray(data.items) ? data.items : [] + }, +} diff --git a/frontend/src/components/BusinessLineSwitcher.vue b/frontend/src/components/BusinessLineSwitcher.vue index c13e03e..9c3fc66 100644 --- a/frontend/src/components/BusinessLineSwitcher.vue +++ b/frontend/src/components/BusinessLineSwitcher.vue @@ -5,7 +5,7 @@ class="bl-ic" :style="{ background: currentBL?.iconBg, color: currentBL?.iconColor }" > - {{ currentBL?.iconText }} + {{ currentBL?.iconText || 'BL' }} {{ currentBL?.name || '未选择' }} {{ currentBL.role }} · 授权 {{ authCount }} 子系统 @@ -32,7 +32,7 @@
{{ bl.ou }} · {{ bl.role }}
✓ - 🔒 无权限 + 无权限 @@ -72,6 +72,9 @@ function onClickOutside(e: MouseEvent) { onMounted(() => { document.addEventListener('click', onClickOutside) + blStore.loadMine().catch(() => { + // 页面其他接口会统一展示登录状态,这里只保持切换器为空态。 + }) }) onUnmounted(() => { diff --git a/frontend/src/components/Layout/AppSidebar.vue b/frontend/src/components/Layout/AppSidebar.vue index 5680ba2..3e81b33 100644 --- a/frontend/src/components/Layout/AppSidebar.vue +++ b/frontend/src/components/Layout/AppSidebar.vue @@ -63,6 +63,12 @@ ▦子系统赋权 + + ▤业务线管理 + + + ▥业务线分配 +