diff --git a/src/api/members.js b/src/api/members.js index 99c1c2f..917e3b4 100644 --- a/src/api/members.js +++ b/src/api/members.js @@ -3,3 +3,6 @@ import { adminHttp } from './http' export const getMembers = () => adminHttp.get('/admin/members') export const upsertMember = (data) => adminHttp.post('/admin/members/upsert', data) export const updateMember = (authentikSub, data) => adminHttp.patch(`/admin/members/${authentikSub}`, data) +export const getMemberPermissionGroups = (authentikSub) => adminHttp.get(`/admin/members/${authentikSub}/permission-groups`) +export const setMemberPermissionGroups = (authentikSub, groupKeys) => + adminHttp.put(`/admin/members/${authentikSub}/permission-groups`, { group_keys: groupKeys }) diff --git a/src/api/permission-admin.js b/src/api/permission-admin.js index 01062fe..cc4d23f 100644 --- a/src/api/permission-admin.js +++ b/src/api/permission-admin.js @@ -2,3 +2,5 @@ import { adminHttp } from './http' export const grantPermission = (data) => adminHttp.post('/admin/permissions/grant', data) export const revokePermission = (data) => adminHttp.post('/admin/permissions/revoke', data) +export const listDirectPermissions = (params) => adminHttp.get('/admin/permissions/direct', { params }) +export const revokeDirectPermissionById = (permissionId) => adminHttp.delete(`/admin/permissions/direct/${permissionId}`) diff --git a/src/api/permission-groups.js b/src/api/permission-groups.js index a78e317..2845f97 100644 --- a/src/api/permission-groups.js +++ b/src/api/permission-groups.js @@ -3,6 +3,7 @@ import { adminHttp } from './http' export const getPermissionGroups = () => adminHttp.get('/admin/permission-groups') export const createPermissionGroup = (data) => adminHttp.post('/admin/permission-groups', data) export const updatePermissionGroup = (groupKey, data) => adminHttp.patch(`/admin/permission-groups/${groupKey}`, data) +export const getPermissionGroupPermissions = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/permissions`) export const addMemberToGroup = (groupKey, authentikSub) => adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`) diff --git a/src/pages/admin/MembersPage.vue b/src/pages/admin/MembersPage.vue index 79e2ca4..7108b69 100644 --- a/src/pages/admin/MembersPage.vue +++ b/src/pages/admin/MembersPage.vue @@ -30,6 +30,11 @@ + + + + + @@ -44,6 +49,11 @@ + + + + + @@ -59,9 +69,17 @@ import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { Refresh } from '@element-plus/icons-vue' -import { getMembers, upsertMember, updateMember } from '@/api/members' +import { + getMembers, + upsertMember, + updateMember, + getMemberPermissionGroups, + setMemberPermissionGroups +} from '@/api/members' +import { getPermissionGroups } from '@/api/permission-groups' const members = ref([]) +const groups = ref([]) const loading = ref(false) const error = ref(false) const errorMsg = ref('') @@ -72,6 +90,7 @@ const creating = ref(false) const createForm = ref({ email: '', display_name: '', + group_keys: [], is_active: true, sync_to_authentik: true }) @@ -85,6 +104,7 @@ const editForm = ref({ authentik_sub: '', email: '', display_name: '', + group_keys: [], is_active: true, sync_to_authentik: true }) @@ -93,8 +113,9 @@ async function load() { loading.value = true error.value = false try { - const res = await getMembers() - members.value = res.data?.items || [] + const [membersRes, groupsRes] = await Promise.all([getMembers(), getPermissionGroups()]) + members.value = membersRes.data?.items || [] + groups.value = groupsRes.data?.items || [] } catch (err) { error.value = true errorMsg.value = err.response?.data?.detail || '載入失敗,請稍後再試' @@ -107,19 +128,27 @@ function resetCreateForm() { createForm.value = { email: '', display_name: '', + group_keys: [], is_active: true, sync_to_authentik: true } } -function openEdit(row) { +async function openEdit(row) { editForm.value = { authentik_sub: row.authentik_sub, email: row.email || '', display_name: row.display_name || '', + group_keys: [], is_active: !!row.is_active, sync_to_authentik: true } + try { + const res = await getMemberPermissionGroups(row.authentik_sub) + editForm.value.group_keys = res.data?.group_keys || [] + } catch (err) { + ElMessage.warning('載入會員群組失敗,仍可先編輯基本資料') + } showEditDialog.value = true } @@ -128,6 +157,7 @@ function resetEditForm() { authentik_sub: '', email: '', display_name: '', + group_keys: [], is_active: true, sync_to_authentik: true } @@ -138,7 +168,11 @@ async function handleCreate() { if (!valid) return creating.value = true try { - await upsertMember({ ...createForm.value }) + const created = await upsertMember({ ...createForm.value }) + const createdSub = created.data?.authentik_sub + if (createdSub && createForm.value.group_keys.length > 0) { + await setMemberPermissionGroups(createdSub, createForm.value.group_keys) + } ElMessage.success('新增會員成功') showCreateDialog.value = false resetCreateForm() @@ -160,6 +194,7 @@ async function handleEdit() { is_active: editForm.value.is_active, sync_to_authentik: editForm.value.sync_to_authentik }) + await setMemberPermissionGroups(editForm.value.authentik_sub, editForm.value.group_keys || []) ElMessage.success('更新會員成功') showEditDialog.value = false await load() diff --git a/src/pages/admin/PermissionGroupsPage.vue b/src/pages/admin/PermissionGroupsPage.vue index 51cf4df..954faa8 100644 --- a/src/pages/admin/PermissionGroupsPage.vue +++ b/src/pages/admin/PermissionGroupsPage.vue @@ -20,43 +20,13 @@ - - -
- - - - - - - - - - - - - - 加入群組 - - - - - - -
-
-
@@ -165,6 +135,23 @@ 儲存 + + +
+ Group: {{ selectedGroupKey }} +
+ + + + + + + + + +
@@ -176,7 +163,7 @@ import { getPermissionGroups, createPermissionGroup, updatePermissionGroup, - addMemberToGroup, + getPermissionGroupPermissions, groupGrant, groupRevoke } from '@/api/permission-groups' @@ -184,14 +171,12 @@ 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 activeTab = ref('groups') const systems = ref([]) const modules = ref([]) const companies = ref([]) const sites = ref([]) -const members = ref([]) const actionOptions = ['view', 'edit', 'manage', 'admin'] const filteredModuleOptions = computed(() => { @@ -237,18 +222,16 @@ async function loadGroups() { } async function loadCatalogs() { - const [systemsRes, modulesRes, companiesRes, sitesRes, membersRes] = await Promise.all([ + const [systemsRes, modulesRes, companiesRes, sitesRes] = await Promise.all([ getSystems(), getModules(), getCompanies(), - getSites(), - getMembers() + getSites() ]) 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 || [] } // Create Group @@ -313,25 +296,22 @@ async function handleEditGroup() { } } -// Add Member -const memberForm = reactive({ groupKey: '', authentikSub: '' }) -const addingMember = ref(false) -const memberError = ref('') -const memberSuccess = ref('') +const showPermissionsDialog = ref(false) +const loadingGroupPermissions = ref(false) +const selectedGroupPermissions = ref([]) +const selectedGroupKey = ref('') -async function handleAddMember() { - memberError.value = '' - memberSuccess.value = '' - addingMember.value = true +async function openPermissionsDialog(row) { + selectedGroupKey.value = row.group_key + showPermissionsDialog.value = true + loadingGroupPermissions.value = true try { - await addMemberToGroup(memberForm.groupKey, memberForm.authentikSub) - memberSuccess.value = '加入成功' - memberForm.groupKey = '' - memberForm.authentikSub = '' + const res = await getPermissionGroupPermissions(row.group_key) + selectedGroupPermissions.value = res.data?.items || [] } catch (err) { - memberError.value = '加入失敗,請稍後再試' + ElMessage.error('載入群組權限失敗') } finally { - addingMember.value = false + loadingGroupPermissions.value = false } } diff --git a/src/pages/permissions/PermissionAdminPage.vue b/src/pages/permissions/PermissionAdminPage.vue index 0dbc4ac..f8cca8e 100644 --- a/src/pages/permissions/PermissionAdminPage.vue +++ b/src/pages/permissions/PermissionAdminPage.vue @@ -154,6 +154,40 @@
+ + + + + + + + + + + + + + + + + + + + @@ -166,6 +200,7 @@ import { getModules } from '@/api/modules' import { getCompanies } from '@/api/companies' import { getSites } from '@/api/sites' import { getMembers } from '@/api/members' +import { listDirectPermissions, revokeDirectPermissionById } from '@/api/permission-admin' const permissionStore = usePermissionStore() @@ -176,6 +211,10 @@ const companies = ref([]) const sites = ref([]) const members = ref([]) const actionOptions = ['view', 'edit', 'manage', 'admin'] +const listFilters = reactive({ keyword: '', scope_type: '' }) +const listLoading = ref(false) +const directPermissions = ref([]) +const revokeRowLoadingId = ref('') // Grant const grantFormRef = ref() @@ -237,6 +276,7 @@ async function handleGrant() { const result = await permissionStore.grant({ ...grantForm }) grantSuccess.value = `授權成功(ID: ${result.permission_id})` ElMessage.success('Grant 成功') + await loadDirectPermissionList() } catch (err) { grantError.value = formatAdminError(err) } finally { @@ -305,6 +345,7 @@ async function handleRevoke() { const result = await permissionStore.revoke({ ...revokeForm }) revokeSuccess.value = `撤銷成功(共刪除 ${result.deleted} 筆)` ElMessage.success('Revoke 成功') + await loadDirectPermissionList() } catch (err) { revokeError.value = formatAdminError(err) } finally { @@ -356,6 +397,39 @@ async function loadCatalogs() { members.value = membersRes.data?.items || [] } +async function loadDirectPermissionList() { + listLoading.value = true + try { + const res = await listDirectPermissions({ + keyword: listFilters.keyword || undefined, + scope_type: listFilters.scope_type || undefined, + limit: 200, + offset: 0 + }) + directPermissions.value = (res.data?.items || []).map(row => ({ + ...row, + created_at: row.created_at ? new Date(row.created_at).toLocaleString() : '' + })) + } catch (err) { + ElMessage.error('載入權限列表失敗') + } finally { + listLoading.value = false + } +} + +async function handleRevokeByRow(row) { + revokeRowLoadingId.value = row.permission_id + try { + await revokeDirectPermissionById(row.permission_id) + ElMessage.success('已撤銷該筆授權') + await loadDirectPermissionList() + } catch (err) { + ElMessage.error('撤銷失敗') + } finally { + revokeRowLoadingId.value = '' + } +} + watch(() => grantForm.scope_type, () => { grantForm.scope_id = '' }) watch(() => grantForm.system, () => { grantForm.module = '' }) watch(() => revokeForm.scope_type, () => { revokeForm.scope_id = '' }) @@ -368,5 +442,7 @@ watch(() => grantForm.authentik_sub, (sub) => { grantForm.display_name = user.display_name || '' }) -onMounted(loadCatalogs) +onMounted(async () => { + await Promise.all([loadCatalogs(), loadDirectPermissionList()]) +})