Files
member-frontend/src/pages/permissions/PermissionAdminPage.vue

373 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>