feat: add business line permissions and Wayne namespace mapping
This commit is contained in:
Vendored
+8
@@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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<BusinessLine[]> {
|
||||
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<BusinessLine[]> {
|
||||
const data = await request('/auth/api/v1/business-lines/all')
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async create(name: string): Promise<BusinessLine> {
|
||||
return request('/auth/api/v1/business-lines', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
},
|
||||
|
||||
async update(id: number, name: string): Promise<BusinessLine> {
|
||||
return request(`/auth/api/v1/business-lines/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
},
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
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<void> {
|
||||
await request('/auth/api/v1/business-lines/authorizations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
|
||||
async listWayneNamespaces(): Promise<WayneNamespace[]> {
|
||||
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<WayneNamespace[]> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
@@ -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))}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export interface UserOption {
|
||||
uid: number
|
||||
username: string
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
async list(): Promise<UserOption[]> {
|
||||
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 : []
|
||||
},
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
class="bl-ic"
|
||||
:style="{ background: currentBL?.iconBg, color: currentBL?.iconColor }"
|
||||
>
|
||||
{{ currentBL?.iconText }}
|
||||
{{ currentBL?.iconText || 'BL' }}
|
||||
</div>
|
||||
<span class="bl-name-trigger">{{ currentBL?.name || '未选择' }}</span>
|
||||
<span class="bl-role" v-if="currentBL">{{ currentBL.role }} · 授权 {{ authCount }} 子系统</span>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="bl-sub">{{ bl.ou }} · {{ bl.role }}</div>
|
||||
</div>
|
||||
<span v-if="bl.authorized" class="bl-check">✓</span>
|
||||
<span v-else class="bl-lock">🔒 无权限</span>
|
||||
<span v-else class="bl-lock">无权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -72,6 +72,9 @@ function onClickOutside(e: MouseEvent) {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onClickOutside)
|
||||
blStore.loadMine().catch(() => {
|
||||
// 页面其他接口会统一展示登录状态,这里只保持切换器为空态。
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -63,6 +63,12 @@
|
||||
<router-link to="/subsystem/authz" class="nav-item" active-class="active">
|
||||
<span class="ic">▦</span>子系统赋权
|
||||
</router-link>
|
||||
<router-link v-if="isPlatformAdmin" to="/business-line/manage" class="nav-item" active-class="active">
|
||||
<span class="ic">▤</span>业务线管理
|
||||
</router-link>
|
||||
<router-link v-if="isBusinessLineAdmin" to="/business-line/assignment" class="nav-item" active-class="active">
|
||||
<span class="ic">▥</span>业务线分配
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">审计</div>
|
||||
@@ -81,9 +87,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const portalExpanded = ref(true)
|
||||
const isPlatformAdmin = computed(() => authStore.user?.is_admin === true)
|
||||
const isBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin)
|
||||
|
||||
const subsystems = [
|
||||
{ name: 'Wayne', label: '多集群发布平台', icon: 'W' },
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { redirectToSSO } from '@/utils/sso'
|
||||
|
||||
export function useAuth() {
|
||||
const authStore = useAuthStore()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await authApi.login({ username, password })
|
||||
const { token, user } = response.data
|
||||
authStore.setAuth(token, user)
|
||||
await businessLineStore.loadMine()
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -19,6 +22,7 @@ export function useAuth() {
|
||||
logoutUrl = response.data.logout_url || ''
|
||||
} finally {
|
||||
authStore.clearAuth()
|
||||
businessLineStore.clear()
|
||||
window.location.assign(logoutUrl || '/login?logged_out=1')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { consumeSSOToken, redirectToSSO } from '@/utils/sso'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
import { authApi } from '@/api/auth'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -46,6 +47,22 @@ const router = createRouter({
|
||||
component: () => import('@/views/subsystem/Authorization.vue'),
|
||||
meta: { title: '子系统赋权' },
|
||||
},
|
||||
{
|
||||
path: 'business-line/permissions',
|
||||
redirect: '/business-line/manage',
|
||||
},
|
||||
{
|
||||
path: 'business-line/manage',
|
||||
name: 'BusinessLineManage',
|
||||
component: () => import('@/views/businessLine/Manage.vue'),
|
||||
meta: { title: '业务线管理' },
|
||||
},
|
||||
{
|
||||
path: 'business-line/assignment',
|
||||
name: 'BusinessLineAssignment',
|
||||
component: () => import('@/views/businessLine/Assignment.vue'),
|
||||
meta: { title: '业务线分配' },
|
||||
},
|
||||
{
|
||||
path: 'audit/login',
|
||||
name: 'LoginAudit',
|
||||
@@ -142,6 +159,7 @@ router.beforeEach(async (to) => {
|
||||
if (token && (!authStore.user || ssoToken)) {
|
||||
try {
|
||||
await authStore.refreshUser()
|
||||
await useBusinessLineStore().loadMine()
|
||||
} catch {
|
||||
authStore.clearAuth()
|
||||
const { data } = await authApi.getConfig()
|
||||
@@ -151,6 +169,8 @@ router.beforeEach(async (to) => {
|
||||
redirectToSSO()
|
||||
return false
|
||||
}
|
||||
} else if (token && !useBusinessLineStore().businessLines.length) {
|
||||
await useBusinessLineStore().loadMine().catch(() => {})
|
||||
}
|
||||
if (to.path === '/login' && token) {
|
||||
return '/'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { businessLineApi, type BusinessLine as ApiBusinessLine } from '@/api/businessLine'
|
||||
|
||||
export interface BusinessLine {
|
||||
id: string
|
||||
id: number
|
||||
name: string
|
||||
ou: string
|
||||
role: string
|
||||
@@ -10,63 +11,19 @@ export interface BusinessLine {
|
||||
iconBg: string
|
||||
iconColor: string
|
||||
authorized: boolean
|
||||
permission?: 0 | 1
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'xinfra-current-bl'
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
const MOCK_BUSINESS_LINES: BusinessLine[] = [
|
||||
{
|
||||
id: 'las',
|
||||
name: 'LAS 业务线',
|
||||
ou: 'ou=las',
|
||||
role: 'SRE',
|
||||
iconText: 'LA',
|
||||
iconBg: '#1A1430',
|
||||
iconColor: '#C9A6FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'kodo',
|
||||
name: 'Kodo 业务线',
|
||||
ou: 'ou=kodo',
|
||||
role: '研发',
|
||||
iconText: 'KO',
|
||||
iconBg: '#0E1C2C',
|
||||
iconColor: '#8EC8FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'lingxi',
|
||||
name: '灵矽 业务线',
|
||||
ou: 'ou=lingxi',
|
||||
role: '未授权',
|
||||
iconText: 'LX',
|
||||
iconBg: '#241B0A',
|
||||
iconColor: '#FFC97A',
|
||||
authorized: false,
|
||||
},
|
||||
{
|
||||
id: 'ltoken',
|
||||
name: 'LTOKEN 业务线',
|
||||
ou: 'ou=ltoken',
|
||||
role: '研发',
|
||||
iconText: 'LT',
|
||||
iconBg: '#1A2A1A',
|
||||
iconColor: '#A6FFB0',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'maas',
|
||||
name: 'MAAS 业务线',
|
||||
ou: 'ou=maas',
|
||||
role: '未授权',
|
||||
iconText: 'MA',
|
||||
iconBg: '#2A1A2A',
|
||||
iconColor: '#FFB0E0',
|
||||
authorized: false,
|
||||
},
|
||||
]
|
||||
const COLORS = [
|
||||
['#1A1430', '#C9A6FF'],
|
||||
['#0E1C2C', '#8EC8FF'],
|
||||
['#241B0A', '#FFC97A'],
|
||||
['#1A2A1A', '#A6FFB0'],
|
||||
['#2A1A2A', '#FFB0E0'],
|
||||
] as const
|
||||
|
||||
function loadSavedBL(): BusinessLine | null {
|
||||
try {
|
||||
@@ -78,30 +35,88 @@ function loadSavedBL(): BusinessLine | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export const useBusinessLineStore = defineStore('businessLine', () => {
|
||||
const businessLines = ref<BusinessLine[]>(MOCK_BUSINESS_LINES)
|
||||
const currentBL = ref<BusinessLine | null>(loadSavedBL())
|
||||
|
||||
// 默认选中第一个已授权的业务线
|
||||
if (!currentBL.value) {
|
||||
currentBL.value = businessLines.value.find((bl) => bl.authorized) || null
|
||||
function saveBL(bl: BusinessLine | null) {
|
||||
if (!bl) {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bl))
|
||||
}
|
||||
|
||||
function initials(name: string) {
|
||||
const compact = name.replace(/\s+/g, '')
|
||||
return compact.slice(0, 2).toUpperCase() || 'BL'
|
||||
}
|
||||
|
||||
function mapBusinessLine(item: ApiBusinessLine, index: number): BusinessLine {
|
||||
const [iconBg, iconColor] = COLORS[index % COLORS.length]
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
ou: `ou=${item.name}`,
|
||||
role: item.permission === 0 ? '管理员' : '普通用户',
|
||||
iconText: initials(item.name),
|
||||
iconBg,
|
||||
iconColor,
|
||||
authorized: true,
|
||||
permission: item.permission,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export const useBusinessLineStore = defineStore('businessLine', () => {
|
||||
const businessLines = ref<BusinessLine[]>([])
|
||||
const currentBL = ref<BusinessLine | null>(loadSavedBL())
|
||||
const loading = ref(false)
|
||||
|
||||
const items = computed(() => businessLines.value)
|
||||
const current = computed(() => currentBL.value)
|
||||
const isCurrentAdmin = computed(() => currentBL.value?.permission === 0)
|
||||
|
||||
const authorizedBLs = computed(() =>
|
||||
businessLines.value.filter((bl) => bl.authorized),
|
||||
)
|
||||
|
||||
function switchBL(id: string) {
|
||||
async function loadMine() {
|
||||
loading.value = true
|
||||
try {
|
||||
const rows = await businessLineApi.listMine()
|
||||
const mapped = rows.map(mapBusinessLine)
|
||||
businessLines.value = mapped
|
||||
|
||||
const savedID = currentBL.value?.id
|
||||
const next = mapped.find((bl) => bl.id === savedID) || mapped[0] || null
|
||||
currentBL.value = next
|
||||
saveBL(next)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function switchBL(id: number) {
|
||||
const bl = businessLines.value.find((b) => b.id === id)
|
||||
if (!bl || !bl.authorized) return
|
||||
currentBL.value = bl
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bl))
|
||||
saveBL(bl)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
businessLines.value = []
|
||||
currentBL.value = null
|
||||
saveBL(null)
|
||||
}
|
||||
|
||||
return {
|
||||
businessLines,
|
||||
items,
|
||||
currentBL,
|
||||
current,
|
||||
loading,
|
||||
isCurrentAdmin,
|
||||
authorizedBLs,
|
||||
loadMine,
|
||||
switchBL,
|
||||
clear,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>业务线分配</h2>
|
||||
<p>{{ currentName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCurrentBusinessLineAdmin" class="assignment-panel">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="用户">
|
||||
<el-select v-model="grantForm.target_user_id" filterable placeholder="选择用户">
|
||||
<el-option v-for="user in users" :key="user.uid" :label="user.username" :value="user.uid" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务线">
|
||||
<el-select v-model="grantForm.target_business_line_id" filterable placeholder="选择业务线">
|
||||
<el-option v-for="item in businessLineStore.items" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="权限">
|
||||
<el-segmented v-model="grantForm.permission" :options="permissionOptions" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="granting" @click="grantPermission">保存分配</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="Wayne namespace">
|
||||
<el-select
|
||||
v-model="selectedWayneNamespaceIds"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="loadingWayneNamespaces"
|
||||
placeholder="选择 Wayne namespace"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in wayneNamespaces"
|
||||
:key="item.id"
|
||||
:label="`${item.name} / ${item.kubeNamespace}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="savingWayneNamespaces" @click="saveWayneNamespaceMapping">保存 Wayne namespace 映射</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="empty-state">需要当前业务线管理员权限</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { businessLineApi, type WayneNamespace } from '@/api/businessLine'
|
||||
import { userApi, type UserOption } from '@/api/user'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const isCurrentBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin)
|
||||
const currentName = computed(() => businessLineStore.current?.name || '未选择业务线')
|
||||
const users = ref<UserOption[]>([])
|
||||
const wayneNamespaces = ref<WayneNamespace[]>([])
|
||||
const selectedWayneNamespaceIds = ref<number[]>([])
|
||||
const granting = ref(false)
|
||||
const loadingWayneNamespaces = ref(false)
|
||||
const savingWayneNamespaces = ref(false)
|
||||
const grantForm = reactive<{
|
||||
target_user_id: number | null
|
||||
target_business_line_id: number | null
|
||||
permission: 0 | 1
|
||||
}>({
|
||||
target_user_id: null,
|
||||
target_business_line_id: null,
|
||||
permission: 1,
|
||||
})
|
||||
|
||||
const permissionOptions = [
|
||||
{ label: '管理员', value: 0 },
|
||||
{ label: '普通用户', value: 1 },
|
||||
]
|
||||
|
||||
watch(
|
||||
() => businessLineStore.current?.id,
|
||||
(id) => {
|
||||
if (id && !grantForm.target_business_line_id) {
|
||||
grantForm.target_business_line_id = id
|
||||
}
|
||||
if (id && isCurrentBusinessLineAdmin.value) {
|
||||
loadWayneNamespaceMapping(id)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
isCurrentBusinessLineAdmin,
|
||||
async (admin) => {
|
||||
if (admin && !users.value.length) {
|
||||
users.value = await userApi.list()
|
||||
}
|
||||
if (admin) {
|
||||
await loadWayneNamespaces()
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (businessLineID) {
|
||||
await loadWayneNamespaceMapping(businessLineID)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (isCurrentBusinessLineAdmin.value && !users.value.length) {
|
||||
users.value = await userApi.list()
|
||||
}
|
||||
if (isCurrentBusinessLineAdmin.value) {
|
||||
await loadWayneNamespaces()
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (businessLineID) {
|
||||
await loadWayneNamespaceMapping(businessLineID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function grantPermission() {
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (!businessLineID || !grantForm.target_user_id || !grantForm.target_business_line_id) {
|
||||
ElMessage.warning('请选择用户和业务线')
|
||||
return
|
||||
}
|
||||
granting.value = true
|
||||
try {
|
||||
await businessLineApi.grant({
|
||||
business_line_id: businessLineID,
|
||||
target_user_id: grantForm.target_user_id,
|
||||
target_business_line_id: grantForm.target_business_line_id,
|
||||
permission: grantForm.permission,
|
||||
})
|
||||
ElMessage.success('已保存业务线分配')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存业务线分配失败')
|
||||
} finally {
|
||||
granting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWayneNamespaces() {
|
||||
if (wayneNamespaces.value.length) {
|
||||
return
|
||||
}
|
||||
loadingWayneNamespaces.value = true
|
||||
try {
|
||||
wayneNamespaces.value = await businessLineApi.listWayneNamespaces()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '查询 Wayne namespace 失败')
|
||||
} finally {
|
||||
loadingWayneNamespaces.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWayneNamespaceMapping(businessLineID: number) {
|
||||
loadingWayneNamespaces.value = true
|
||||
try {
|
||||
const mapped = await businessLineApi.listMappedWayneNamespaces(businessLineID)
|
||||
selectedWayneNamespaceIds.value = mapped.map((item) => item.id)
|
||||
} catch (error) {
|
||||
selectedWayneNamespaceIds.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '查询 Wayne namespace 映射失败')
|
||||
} finally {
|
||||
loadingWayneNamespaces.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWayneNamespaceMapping() {
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (!businessLineID) {
|
||||
ElMessage.warning('请选择当前业务线')
|
||||
return
|
||||
}
|
||||
const selected = wayneNamespaces.value.filter((item) => selectedWayneNamespaceIds.value.includes(item.id))
|
||||
savingWayneNamespaces.value = true
|
||||
try {
|
||||
await businessLineApi.replaceMappedWayneNamespaces(businessLineID, selected)
|
||||
ElMessage.success('已保存 Wayne namespace 映射')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存 Wayne namespace 映射失败')
|
||||
} finally {
|
||||
savingWayneNamespaces.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.assignment-panel {
|
||||
max-width: 520px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border-top: 1px solid var(--line);
|
||||
margin: 22px 0 18px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 28px 0;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>业务线管理</h2>
|
||||
<p>平台管理员维护全局业务线</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isPlatformAdmin">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="newName" class="name-input" placeholder="业务线名称" @keyup.enter="createBusinessLine" />
|
||||
<el-button type="primary" :icon="Plus" :loading="savingLine" @click="createBusinessLine">新增</el-button>
|
||||
<el-button :loading="loadingLines" @click="loadAllBusinessLines">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="allBusinessLines" v-loading="loadingLines" class="data-table" empty-text="没有业务线数据">
|
||||
<el-table-column prop="id" label="ID" width="90" />
|
||||
<el-table-column label="名称" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-input v-if="editingId === row.id" v-model="editingName" size="small" @keyup.enter="saveBusinessLine(row.id)" />
|
||||
<span v-else>{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" min-width="210" />
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="editingId === row.id">
|
||||
<el-button size="small" type="primary" @click="saveBusinessLine(row.id)">保存</el-button>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button size="small" :icon="Edit" @click="startEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" :icon="Delete" @click="deleteBusinessLine(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div v-else class="empty-state">需要平台管理员权限</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { businessLineApi, type BusinessLine } from '@/api/businessLine'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const isPlatformAdmin = computed(() => authStore.user?.is_admin === true)
|
||||
|
||||
const allBusinessLines = ref<BusinessLine[]>([])
|
||||
const loadingLines = ref(false)
|
||||
const savingLine = ref(false)
|
||||
const newName = ref('')
|
||||
const editingId = ref<number | null>(null)
|
||||
const editingName = ref('')
|
||||
|
||||
watch(
|
||||
isPlatformAdmin,
|
||||
async (admin) => {
|
||||
if (admin) {
|
||||
await loadAllBusinessLines()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await authStore.refreshUser()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (isPlatformAdmin.value) {
|
||||
await loadAllBusinessLines()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadAllBusinessLines() {
|
||||
loadingLines.value = true
|
||||
try {
|
||||
allBusinessLines.value = await businessLineApi.listAll()
|
||||
} catch (error) {
|
||||
allBusinessLines.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '查询业务线失败')
|
||||
} finally {
|
||||
loadingLines.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createBusinessLine() {
|
||||
const name = newName.value.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入业务线名称')
|
||||
return
|
||||
}
|
||||
savingLine.value = true
|
||||
try {
|
||||
await businessLineApi.create(name)
|
||||
newName.value = ''
|
||||
await loadAllBusinessLines()
|
||||
await businessLineStore.loadMine()
|
||||
ElMessage.success('已新增业务线')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '新增业务线失败')
|
||||
} finally {
|
||||
savingLine.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(row: BusinessLine) {
|
||||
editingId.value = row.id
|
||||
editingName.value = row.name
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId.value = null
|
||||
editingName.value = ''
|
||||
}
|
||||
|
||||
async function saveBusinessLine(id: number) {
|
||||
const name = editingName.value.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入业务线名称')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await businessLineApi.update(id, name)
|
||||
cancelEdit()
|
||||
await loadAllBusinessLines()
|
||||
await businessLineStore.loadMine()
|
||||
ElMessage.success('已更新业务线')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '更新业务线失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBusinessLine(row: BusinessLine) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除业务线 ${row.name}?`, '删除业务线', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
await businessLineApi.remove(row.id)
|
||||
await loadAllBusinessLines()
|
||||
await businessLineStore.loadMine()
|
||||
ElMessage.success('已删除业务线')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除业务线失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 18px 0 14px;
|
||||
}
|
||||
|
||||
.name-input {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 28px 0;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -69,8 +69,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { subsystemApi, type Subsystem } from '@/api/subsystem'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { redirectToSSO } from '@/utils/sso'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
+2
-1
@@ -11,4 +11,5 @@ web/dist/
|
||||
certs/
|
||||
scripts/
|
||||
data/
|
||||
docs/
|
||||
docs/*
|
||||
!docs/mysql-init.sql
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
CREATE DATABASE IF NOT EXISTS authserver
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'localhost' IDENTIFIED BY 'auth';
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'127.0.0.1' IDENTIFIED BY 'auth';
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'192.168.65.%' IDENTIFIED BY 'auth';
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'%' IDENTIFIED BY 'auth';
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE authserver;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `business_lines` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`created_at` datetime(3) DEFAULT NULL,
|
||||
`updated_at` datetime(3) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_business_lines_name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `business_line_users` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`business_line_id` bigint unsigned NOT NULL,
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`permission` bigint NOT NULL DEFAULT '1',
|
||||
`created_at` datetime(3) DEFAULT NULL,
|
||||
`updated_at` datetime(3) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_business_line_users_unique` (`business_line_id`,`user_id`),
|
||||
KEY `idx_business_line_users_business_line_id` (`business_line_id`),
|
||||
KEY `idx_business_line_users_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `business_line_wayne_namespaces` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`business_line_id` bigint unsigned NOT NULL,
|
||||
`wayne_namespace_id` bigint unsigned NOT NULL,
|
||||
`wayne_namespace_name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`kube_namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`created_at` datetime(3) DEFAULT NULL,
|
||||
`updated_at` datetime(3) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_business_line_wayne_namespaces_unique` (`business_line_id`,`wayne_namespace_id`),
|
||||
KEY `idx_business_line_wayne_namespaces_business_line_id` (`business_line_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -15,6 +15,9 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.WayenCredential{},
|
||||
&model.BusinessLine{},
|
||||
&model.BusinessLineUser{},
|
||||
&model.BusinessLineWayneNamespace{},
|
||||
&model.AccessToken{},
|
||||
&model.AuditLog{},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BusinessLineHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type BusinessLineWithPermission struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Permission int `json:"permission"`
|
||||
}
|
||||
|
||||
type GrantBusinessLinePermissionRequest struct {
|
||||
BusinessLineID uint64 `json:"business_line_id" binding:"required"`
|
||||
TargetUserID uint64 `json:"target_user_id" binding:"required"`
|
||||
TargetBusinessLineID uint64 `json:"target_business_line_id" binding:"required"`
|
||||
Permission int `json:"permission"`
|
||||
}
|
||||
|
||||
type BusinessLinePayload struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
type WayneNamespaceBindingPayload struct {
|
||||
Namespaces []WayneNamespaceBindingItem `json:"namespaces"`
|
||||
}
|
||||
|
||||
type WayneNamespaceBindingItem struct {
|
||||
ID uint64 `json:"id" binding:"required"`
|
||||
Name string `json:"name"`
|
||||
KubeNamespace string `json:"kubeNamespace"`
|
||||
}
|
||||
|
||||
func NewBusinessLineHandler(db *gorm.DB) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IsAdmin {
|
||||
var rows []model.BusinessLine
|
||||
if err := h.db.Order("id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]BusinessLineWithPermission, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, BusinessLineWithPermission{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
||||
Permission: 0,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
return
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ID uint64
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Permission int
|
||||
}
|
||||
if err := h.db.
|
||||
Table("business_line_users").
|
||||
Select("business_lines.id, business_lines.name, business_lines.created_at, business_lines.updated_at, business_line_users.permission").
|
||||
Joins("JOIN business_lines ON business_lines.id = business_line_users.business_line_id").
|
||||
Where("business_line_users.user_id = ?", claims.UserID).
|
||||
Order("business_lines.id ASC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]BusinessLineWithPermission, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, BusinessLineWithPermission{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
||||
Permission: row.Permission,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListAll(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var rows []model.BusinessLine
|
||||
if err := h.db.Order("id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, gin.H{
|
||||
"id": row.ID,
|
||||
"name": row.Name,
|
||||
"created_at": row.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": row.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) Create(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req BusinessLinePayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
item := model.BusinessLine{Name: req.Name}
|
||||
if err := h.db.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeBusinessLine(c, item)
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) Update(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req BusinessLinePayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var item model.BusinessLine
|
||||
if err := h.db.First(&item, "id = ?", c.Param("id")).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Model(&item).Update("name", req.Name).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
item.Name = req.Name
|
||||
writeBusinessLine(c, item)
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) Delete(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var item model.BusinessLine
|
||||
if err := h.db.First(&item, "id = ?", c.Param("id")).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("business_line_id = ?", item.ID).Delete(&model.BusinessLineUser{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("business_line_id = ?", item.ID).Delete(&model.BusinessLineWayneNamespace{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&item).Error
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
|
||||
var req GrantBusinessLinePermissionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Permission != 0 && req.Permission != 1 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "permission must be 0 or 1"})
|
||||
return
|
||||
}
|
||||
|
||||
if !claims.IsAdmin {
|
||||
var currentBinding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", req.BusinessLineID, claims.UserID, 0).
|
||||
First(¤tBinding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", req.BusinessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var targetUser model.User
|
||||
if err := h.db.Where("id = ? AND deleted_at IS NULL", req.TargetUserID).First(&targetUser).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var targetBusinessLine model.BusinessLine
|
||||
if err := h.db.First(&targetBusinessLine, "id = ?", req.TargetBusinessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "target business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var binding model.BusinessLineUser
|
||||
err := h.db.Where("business_line_id = ? AND user_id = ?", req.TargetBusinessLineID, req.TargetUserID).First(&binding).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
binding = model.BusinessLineUser{
|
||||
BusinessLineID: req.TargetBusinessLineID,
|
||||
UserID: req.TargetUserID,
|
||||
Permission: req.Permission,
|
||||
}
|
||||
if err := h.db.Create(&binding).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else if binding.Permission != req.Permission {
|
||||
if err := h.db.Model(&binding).Update("permission", req.Permission).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
binding.Permission = req.Permission
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": binding.ID,
|
||||
"business_line_id": binding.BusinessLineID,
|
||||
"user_id": binding.UserID,
|
||||
"permission": binding.Permission,
|
||||
"created_at": binding.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": binding.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListWayneNamespaces(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
var rows []model.BusinessLineWayneNamespace
|
||||
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, gin.H{
|
||||
"id": row.WayneNamespaceID,
|
||||
"name": row.WayneNamespaceName,
|
||||
"kubeNamespace": row.KubeNamespace,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ReplaceWayneNamespaces(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
var req WayneNamespaceBindingPayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("business_line_id = ?", businessLineID).Delete(&model.BusinessLineWayneNamespace{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range req.Namespaces {
|
||||
row := model.BusinessLineWayneNamespace{
|
||||
BusinessLineID: businessLineID,
|
||||
WayneNamespaceID: item.ID,
|
||||
WayneNamespaceName: item.Name,
|
||||
KubeNamespace: item.KubeNamespace,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) canManageBusinessLine(c *gin.Context, businessLineID uint64) bool {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return false
|
||||
}
|
||||
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
if claims.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
var binding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", businessLineID, claims.UserID, 0).
|
||||
First(&binding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
|
||||
return false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseBusinessLineID(c *gin.Context) (uint64, bool) {
|
||||
value, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid business line id"})
|
||||
return 0, false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func requirePlatformAdmin(c *gin.Context) bool {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return false
|
||||
}
|
||||
if !claims.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "platform admin required"})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeBusinessLine(c *gin.Context, item model.BusinessLine) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": item.ID,
|
||||
"name": item.Name,
|
||||
"created_at": item.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": item.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
@@ -3,13 +3,18 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct{}
|
||||
type UserHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserHandler() *UserHandler {
|
||||
return &UserHandler{}
|
||||
func NewUserHandler(db *gorm.DB) *UserHandler {
|
||||
return &UserHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *UserHandler) Me(c *gin.Context) {
|
||||
@@ -25,3 +30,20 @@ func (h *UserHandler) Me(c *gin.Context) {
|
||||
"is_admin": claims.IsAdmin,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
var users []model.User
|
||||
if err := h.db.Where("deleted_at IS NULL").Order("id ASC").Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]gin.H, 0, len(users))
|
||||
for _, user := range users {
|
||||
items = append(items, gin.H{
|
||||
"uid": user.ID,
|
||||
"username": user.Username,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/auth"
|
||||
@@ -40,7 +42,12 @@ func (h *WayenHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.wayen.Login(email, claims.Username)
|
||||
refOverride, ok := h.resolveWayneRef(c, claims)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.wayen.Login(email, claims.Username, refOverride)
|
||||
if err != nil {
|
||||
h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error())
|
||||
switch {
|
||||
@@ -73,6 +80,54 @@ func (h *WayenHandler) Login(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, result.TargetURL)
|
||||
}
|
||||
|
||||
func (h *WayenHandler) resolveWayneRef(c *gin.Context, claims *auth.Claims) (string, bool) {
|
||||
rawBusinessLineID := strings.TrimSpace(c.Query("business_line_id"))
|
||||
if rawBusinessLineID == "" {
|
||||
return "", true
|
||||
}
|
||||
|
||||
businessLineID, err := strconv.ParseUint(rawBusinessLineID, 10, 64)
|
||||
if err != nil || businessLineID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid business_line_id"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return "", false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
|
||||
if !claims.IsAdmin {
|
||||
var binding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ?", businessLineID, claims.UserID).First(&binding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have business line permission"})
|
||||
return "", false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
var mapping model.BusinessLineWayneNamespace
|
||||
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").First(&mapping).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", true
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
if mapping.WayneNamespaceID == 0 {
|
||||
return "", true
|
||||
}
|
||||
return fmt.Sprintf("/portal/namespace/%d/app", mapping.WayneNamespaceID), true
|
||||
}
|
||||
|
||||
func (h *WayenHandler) GetCredential(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
|
||||
@@ -32,6 +32,32 @@ type WayenCredential struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
type BusinessLine struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:128;not null;uniqueIndex" json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BusinessLineUser struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;uniqueIndex:idx_business_line_users_unique,priority:1;index" json:"business_line_id"`
|
||||
UserID uint64 `gorm:"not null;uniqueIndex:idx_business_line_users_unique,priority:2;index" json:"user_id"`
|
||||
Permission int `gorm:"not null;default:1" json:"permission"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BusinessLineWayneNamespace struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;uniqueIndex:idx_business_line_wayne_namespaces_unique,priority:1;index" json:"business_line_id"`
|
||||
WayneNamespaceID uint64 `gorm:"not null;uniqueIndex:idx_business_line_wayne_namespaces_unique,priority:2" json:"wayne_namespace_id"`
|
||||
WayneNamespaceName string `gorm:"size:128;not null;default:''" json:"wayne_namespace_name"`
|
||||
KubeNamespace string `gorm:"size:128;not null;default:''" json:"kube_namespace"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AccessToken struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
UserID uint64 `gorm:"not null;index" json:"user_id"`
|
||||
|
||||
@@ -73,7 +73,8 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
|
||||
healthHandler := handler.NewHealthHandler(deps.DB)
|
||||
authHandler := handler.NewAuthHandler(deps.Config, authService)
|
||||
userHandler := handler.NewUserHandler()
|
||||
userHandler := handler.NewUserHandler(deps.DB)
|
||||
businessLineHandler := handler.NewBusinessLineHandler(deps.DB)
|
||||
wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService)
|
||||
wayneRoleBindingHandler := handler.NewWayneRoleBindingHandler(wayneRoleBindingService, auditService)
|
||||
clouddmHandler := handler.NewCloudDMHandler(deps.Config, auditService)
|
||||
@@ -100,6 +101,15 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
protected := v1.Group("")
|
||||
protected.Use(handler.AuthMiddleware(deps.Config))
|
||||
protected.GET("/users/me", userHandler.Me)
|
||||
protected.GET("/users", userHandler.List)
|
||||
protected.GET("/business-lines", businessLineHandler.ListCurrentUserBusinessLines)
|
||||
protected.GET("/business-lines/all", businessLineHandler.ListAll)
|
||||
protected.POST("/business-lines", businessLineHandler.Create)
|
||||
protected.PUT("/business-lines/:id", businessLineHandler.Update)
|
||||
protected.DELETE("/business-lines/:id", businessLineHandler.Delete)
|
||||
protected.POST("/business-lines/authorizations", businessLineHandler.GrantPermission)
|
||||
protected.GET("/business-lines/:id/wayne-namespaces", businessLineHandler.ListWayneNamespaces)
|
||||
protected.PUT("/business-lines/:id/wayne-namespaces", businessLineHandler.ReplaceWayneNamespaces)
|
||||
protected.GET("/wayen/login", wayenHandler.Login)
|
||||
protected.GET("/wayen/credential", wayenHandler.GetCredential)
|
||||
protected.PUT("/wayen/credential", wayenHandler.SaveCredential)
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewWayenService(cfg config.Config, db *gorm.DB) *WayenService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WayenService) Login(email, username string) (*WayenLoginResult, error) {
|
||||
func (s *WayenService) Login(email, username, refOverride string) (*WayenLoginResult, error) {
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return nil, ErrWayenEmailMissing
|
||||
@@ -58,7 +58,7 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error)
|
||||
oauthLoginURL = strings.TrimSpace(s.cfg.OAuthRedirectURI)
|
||||
}
|
||||
if oauthLoginURL != "" && strings.TrimSpace(s.cfg.WayenTargetURL) != "" {
|
||||
target, err := s.oauthLoginURL(oauthLoginURL, s.cfg.WayenTargetURL)
|
||||
target, err := s.oauthLoginURL(oauthLoginURL, s.cfg.WayenTargetURL, refOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, error) {
|
||||
func (s *WayenService) oauthLoginURL(redirectURI, targetURL, refOverride string) (string, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(redirectURI))
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -124,7 +124,7 @@ func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, err
|
||||
next.Path = "/sign-in"
|
||||
}
|
||||
values := next.Query()
|
||||
values.Set("ref", defaultConfigValue(s.cfg.WayenOAuthRef, "/portal/namespace/1/app"))
|
||||
values.Set("ref", defaultConfigValue(refOverride, defaultConfigValue(s.cfg.WayenOAuthRef, "/portal/namespace/1/app")))
|
||||
next.RawQuery = values.Encode()
|
||||
|
||||
query := parsed.Query()
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestWayenLoginUsesDedicatedOAuthLoginURL(t *testing.T) {
|
||||
WayenOAuthRef: "/portal/namespace/1/app",
|
||||
}, nil)
|
||||
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com")
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func TestWayenLoginFallsBackToOAuthRedirectURI(t *testing.T) {
|
||||
WayenTargetURL: "http://218.11.5.223:32000/",
|
||||
}, nil)
|
||||
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com")
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
@@ -67,3 +67,27 @@ func TestWayenLoginFallsBackToOAuthRedirectURI(t *testing.T) {
|
||||
t.Fatalf("target host = %q, want fallback redirect host", parsed.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayenLoginUsesRefOverride(t *testing.T) {
|
||||
service := NewWayenService(config.Config{
|
||||
WayenOAuthLoginURL: "http://218.11.5.223:32000/login/oauth2/oauth2",
|
||||
WayenTargetURL: "http://218.11.5.223:32000/",
|
||||
WayenOAuthRef: "/portal/namespace/1/app",
|
||||
}, nil)
|
||||
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com", "/portal/namespace/3/app")
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
parsed, err := url.Parse(result.TargetURL)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid target url: %v", err)
|
||||
}
|
||||
parsedNext, err := url.Parse(parsed.Query().Get("next"))
|
||||
if err != nil {
|
||||
t.Fatalf("invalid next url: %v", err)
|
||||
}
|
||||
if parsedNext.Query().Get("ref") != "/portal/namespace/3/app" {
|
||||
t.Fatalf("next ref = %q", parsedNext.Query().Get("ref"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user