373 lines
13 KiB
Vue
373 lines
13 KiB
Vue
<template>
|
||
<div>
|
||
<h2 class="text-xl font-bold text-gray-800 mb-6">權限管理</h2>
|
||
|
||
<!-- Grant / Revoke -->
|
||
<el-tabs v-model="activeTab" type="border-card" class="shadow-sm">
|
||
<!-- Grant Tab -->
|
||
<el-tab-pane label="Grant 授權" name="grant">
|
||
<el-form
|
||
ref="grantFormRef"
|
||
:model="grantForm"
|
||
:rules="grantRules"
|
||
label-width="130px"
|
||
class="max-w-xl mt-4"
|
||
@submit.prevent="handleGrant"
|
||
>
|
||
<el-form-item label="Authentik Sub" prop="authentik_sub">
|
||
<el-select v-model="grantForm.authentik_sub" filterable allow-create default-first-option placeholder="選擇會員或輸入 sub" style="width: 100%">
|
||
<el-option v-for="m in members" :key="m.authentik_sub" :label="`${m.display_name || m.email || '(no-name)'} (${m.authentik_sub})`" :value="m.authentik_sub" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Email" prop="email">
|
||
<el-input v-model="grantForm.email" placeholder="user@example.com" />
|
||
</el-form-item>
|
||
<el-form-item label="顯示名稱" prop="display_name">
|
||
<el-input v-model="grantForm.display_name" placeholder="User Name" />
|
||
</el-form-item>
|
||
<el-form-item label="Scope 類型" prop="scope_type">
|
||
<el-select v-model="grantForm.scope_type" placeholder="選擇 Scope 類型">
|
||
<el-option label="Company" value="company" />
|
||
<el-option label="Site" value="site" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Scope ID" prop="scope_id">
|
||
<el-select v-model="grantForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
|
||
<el-option v-for="s in grantScopeOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="系統" prop="system">
|
||
<el-select v-model="grantForm.system" placeholder="選擇系統" filterable style="width: 100%">
|
||
<el-option v-for="s in systems" :key="s.system_key" :label="`${s.name} (${s.system_key})`" :value="s.system_key" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="模組(選填)" prop="module">
|
||
<el-select v-model="grantForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
|
||
<el-option v-for="m in grantModuleOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="操作" prop="action">
|
||
<el-select v-model="grantForm.action" filterable allow-create default-first-option style="width: 100%">
|
||
<el-option v-for="a in actionOptions" :key="a" :label="a" :value="a" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-alert
|
||
v-if="grantError"
|
||
:title="grantError"
|
||
type="error"
|
||
show-icon
|
||
:closable="false"
|
||
class="mb-4"
|
||
/>
|
||
<el-alert
|
||
v-if="grantSuccess"
|
||
:title="grantSuccess"
|
||
type="success"
|
||
show-icon
|
||
:closable="false"
|
||
class="mb-4"
|
||
/>
|
||
|
||
<el-form-item>
|
||
<el-button
|
||
type="primary"
|
||
native-type="submit"
|
||
:loading="grantLoading"
|
||
>
|
||
Grant 授權
|
||
</el-button>
|
||
<el-button @click="resetGrant">清除</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-tab-pane>
|
||
|
||
<!-- Revoke Tab -->
|
||
<el-tab-pane label="Revoke 撤銷" name="revoke">
|
||
<el-form
|
||
ref="revokeFormRef"
|
||
:model="revokeForm"
|
||
:rules="revokeRules"
|
||
label-width="130px"
|
||
class="max-w-xl mt-4"
|
||
@submit.prevent="handleRevoke"
|
||
>
|
||
<el-form-item label="Authentik Sub" prop="authentik_sub">
|
||
<el-select v-model="revokeForm.authentik_sub" filterable allow-create default-first-option placeholder="選擇會員或輸入 sub" style="width: 100%">
|
||
<el-option v-for="m in members" :key="m.authentik_sub" :label="`${m.display_name || m.email || '(no-name)'} (${m.authentik_sub})`" :value="m.authentik_sub" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Scope 類型" prop="scope_type">
|
||
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
||
<el-option label="Company" value="company" />
|
||
<el-option label="Site" value="site" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Scope ID" prop="scope_id">
|
||
<el-select v-model="revokeForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
|
||
<el-option v-for="s in revokeScopeOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="系統" prop="system">
|
||
<el-select v-model="revokeForm.system" placeholder="選擇系統" filterable style="width: 100%">
|
||
<el-option v-for="s in systems" :key="s.system_key" :label="`${s.name} (${s.system_key})`" :value="s.system_key" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="模組(選填)" prop="module">
|
||
<el-select v-model="revokeForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
|
||
<el-option v-for="m in revokeModuleOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="操作" prop="action">
|
||
<el-select v-model="revokeForm.action" filterable allow-create default-first-option style="width: 100%">
|
||
<el-option v-for="a in actionOptions" :key="a" :label="a" :value="a" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-alert
|
||
v-if="revokeError"
|
||
:title="revokeError"
|
||
type="error"
|
||
show-icon
|
||
:closable="false"
|
||
class="mb-4"
|
||
/>
|
||
<el-alert
|
||
v-if="revokeSuccess"
|
||
:title="revokeSuccess"
|
||
type="success"
|
||
show-icon
|
||
:closable="false"
|
||
class="mb-4"
|
||
/>
|
||
|
||
<el-form-item>
|
||
<el-button
|
||
type="danger"
|
||
native-type="submit"
|
||
:loading="revokeLoading"
|
||
>
|
||
Revoke 撤銷
|
||
</el-button>
|
||
<el-button @click="resetRevoke">清除</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { usePermissionStore } from '@/stores/permission'
|
||
import { getSystems } from '@/api/systems'
|
||
import { getModules } from '@/api/modules'
|
||
import { getCompanies } from '@/api/companies'
|
||
import { getSites } from '@/api/sites'
|
||
import { getMembers } from '@/api/members'
|
||
|
||
const permissionStore = usePermissionStore()
|
||
|
||
const activeTab = ref('grant')
|
||
const systems = ref([])
|
||
const modules = ref([])
|
||
const companies = ref([])
|
||
const sites = ref([])
|
||
const members = ref([])
|
||
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
||
|
||
// Grant
|
||
const grantFormRef = ref()
|
||
const grantLoading = ref(false)
|
||
const grantError = ref('')
|
||
const grantSuccess = ref('')
|
||
|
||
const grantForm = reactive({
|
||
authentik_sub: '',
|
||
email: '',
|
||
display_name: '',
|
||
scope_type: '',
|
||
scope_id: '',
|
||
system: '',
|
||
module: '',
|
||
action: ''
|
||
})
|
||
|
||
const grantModuleOptions = computed(() => {
|
||
if (!grantForm.system) return []
|
||
return modules.value
|
||
.filter(m => m.system_key === grantForm.system && !m.module_key.endsWith('.__system__'))
|
||
.map(m => ({
|
||
value: m.module_key.startsWith(`${grantForm.system}.`)
|
||
? m.module_key.slice(grantForm.system.length + 1)
|
||
: m.module_key,
|
||
label: `${m.name} (${m.module_key})`
|
||
}))
|
||
})
|
||
|
||
const grantScopeOptions = computed(() => {
|
||
if (grantForm.scope_type === 'company') {
|
||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
||
}
|
||
if (grantForm.scope_type === 'site') {
|
||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||
}
|
||
return []
|
||
})
|
||
|
||
const required = { required: true, message: '必填', trigger: 'blur' }
|
||
const grantRules = {
|
||
authentik_sub: [required],
|
||
email: [required],
|
||
display_name: [required],
|
||
scope_type: [required],
|
||
scope_id: [required],
|
||
system: [required],
|
||
action: [required]
|
||
}
|
||
|
||
async function handleGrant() {
|
||
const valid = await grantFormRef.value.validate().catch(() => false)
|
||
if (!valid) return
|
||
grantLoading.value = true
|
||
grantError.value = ''
|
||
grantSuccess.value = ''
|
||
try {
|
||
const result = await permissionStore.grant({ ...grantForm })
|
||
grantSuccess.value = `授權成功(ID: ${result.permission_id})`
|
||
ElMessage.success('Grant 成功')
|
||
} catch (err) {
|
||
grantError.value = formatAdminError(err)
|
||
} finally {
|
||
grantLoading.value = false
|
||
}
|
||
}
|
||
|
||
function resetGrant() {
|
||
grantFormRef.value?.resetFields()
|
||
grantError.value = ''
|
||
grantSuccess.value = ''
|
||
}
|
||
|
||
// Revoke
|
||
const revokeFormRef = ref()
|
||
const revokeLoading = ref(false)
|
||
const revokeError = ref('')
|
||
const revokeSuccess = ref('')
|
||
|
||
const revokeForm = reactive({
|
||
authentik_sub: '',
|
||
scope_type: '',
|
||
scope_id: '',
|
||
system: '',
|
||
module: '',
|
||
action: ''
|
||
})
|
||
|
||
const revokeModuleOptions = computed(() => {
|
||
if (!revokeForm.system) return []
|
||
return modules.value
|
||
.filter(m => m.system_key === revokeForm.system && !m.module_key.endsWith('.__system__'))
|
||
.map(m => ({
|
||
value: m.module_key.startsWith(`${revokeForm.system}.`)
|
||
? m.module_key.slice(revokeForm.system.length + 1)
|
||
: m.module_key,
|
||
label: `${m.name} (${m.module_key})`
|
||
}))
|
||
})
|
||
|
||
const revokeScopeOptions = computed(() => {
|
||
if (revokeForm.scope_type === 'company') {
|
||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
||
}
|
||
if (revokeForm.scope_type === 'site') {
|
||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||
}
|
||
return []
|
||
})
|
||
|
||
const revokeRules = {
|
||
authentik_sub: [required],
|
||
scope_type: [required],
|
||
scope_id: [required],
|
||
system: [required],
|
||
action: [required]
|
||
}
|
||
|
||
async function handleRevoke() {
|
||
const valid = await revokeFormRef.value.validate().catch(() => false)
|
||
if (!valid) return
|
||
revokeLoading.value = true
|
||
revokeError.value = ''
|
||
revokeSuccess.value = ''
|
||
try {
|
||
const result = await permissionStore.revoke({ ...revokeForm })
|
||
revokeSuccess.value = `撤銷成功(共刪除 ${result.deleted} 筆)`
|
||
ElMessage.success('Revoke 成功')
|
||
} catch (err) {
|
||
revokeError.value = formatAdminError(err)
|
||
} finally {
|
||
revokeLoading.value = false
|
||
}
|
||
}
|
||
|
||
function resetRevoke() {
|
||
revokeFormRef.value?.resetFields()
|
||
revokeError.value = ''
|
||
revokeSuccess.value = ''
|
||
}
|
||
|
||
function formatAdminError(err) {
|
||
const status = err.response?.status
|
||
const detail = err.response?.data?.detail
|
||
const map = {
|
||
invalid_client: '無效的 Client Key',
|
||
invalid_api_key: '無效的 API Key',
|
||
client_expired: 'Client 已過期',
|
||
origin_not_allowed: '來源 Origin 不允許',
|
||
ip_not_allowed: 'IP 不在白名單',
|
||
path_not_allowed: '路徑不允許',
|
||
internal_secret_not_configured: '後端設定缺失(internal secret)',
|
||
authentik_admin_not_configured: '後端設定缺失(authentik admin)',
|
||
user_not_found: '找不到該使用者'
|
||
}
|
||
if (detail && map[detail]) return map[detail]
|
||
if (detail) return `錯誤:${detail}`
|
||
if (status === 401) return '認證失敗,請檢查 Client Key / API Key'
|
||
if (status === 403) return '存取被拒(IP 或 Origin 限制)'
|
||
if (status === 404) return '找不到該使用者'
|
||
if (status === 503) return '後端設定不完整,請聯絡管理員'
|
||
return '操作失敗,請稍後再試'
|
||
}
|
||
|
||
async function loadCatalogs() {
|
||
const [systemsRes, modulesRes, companiesRes, sitesRes, membersRes] = await Promise.all([
|
||
getSystems(),
|
||
getModules(),
|
||
getCompanies(),
|
||
getSites(),
|
||
getMembers()
|
||
])
|
||
systems.value = systemsRes.data?.items || []
|
||
modules.value = modulesRes.data?.items || []
|
||
companies.value = companiesRes.data?.items || []
|
||
sites.value = sitesRes.data?.items || []
|
||
members.value = membersRes.data?.items || []
|
||
}
|
||
|
||
watch(() => grantForm.scope_type, () => { grantForm.scope_id = '' })
|
||
watch(() => grantForm.system, () => { grantForm.module = '' })
|
||
watch(() => revokeForm.scope_type, () => { revokeForm.scope_id = '' })
|
||
watch(() => revokeForm.system, () => { revokeForm.module = '' })
|
||
|
||
watch(() => grantForm.authentik_sub, (sub) => {
|
||
const user = members.value.find(m => m.authentik_sub === sub)
|
||
if (!user) return
|
||
grantForm.email = user.email || ''
|
||
grantForm.display_name = user.display_name || ''
|
||
})
|
||
|
||
onMounted(loadCatalogs)
|
||
</script>
|