From 31fff92e195fa565108f684ac274726db5afceb8 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 30 Mar 2026 03:33:50 +0800 Subject: [PATCH] feat(flow): auto-resolve authentik sub and improve admin dropdown UX --- backend/app/api/admin_catalog.py | 18 +++++++++++++--- backend/app/schemas/catalog.py | 2 +- .../app/services/authentik_admin_service.py | 9 ++++++-- frontend/src/pages/admin/MembersPage.vue | 5 +---- frontend/src/pages/admin/ModulesPage.vue | 11 +++++++--- .../src/pages/admin/PermissionGroupsPage.vue | 21 +++++++++++++++---- .../pages/permissions/PermissionAdminPage.vue | 8 +++++-- 7 files changed, 55 insertions(+), 19 deletions(-) diff --git a/backend/app/api/admin_catalog.py b/backend/app/api/admin_catalog.py index 19fb4eb..3ee0d84 100644 --- a/backend/app/api/admin_catalog.py +++ b/backend/app/api/admin_catalog.py @@ -83,7 +83,11 @@ def _sync_member_to_authentik( display_name=display_name, is_active=is_active, ) - return {"authentik_user_id": result.user_id, "sync_action": result.action} + return { + "authentik_user_id": result.user_id, + "sync_action": result.action, + "authentik_sub": result.authentik_sub or "", + } @router.get("/systems") @@ -332,17 +336,25 @@ def upsert_member( db: Session = Depends(get_db), ) -> MemberItem: users_repo = UsersRepository(db) + resolved_sub = payload.authentik_sub authentik_user_id = None if payload.sync_to_authentik: + seed_sub = payload.authentik_sub or (payload.email or "") + if not seed_sub: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="authentik_sub_or_email_required") sync = _sync_member_to_authentik( - authentik_sub=payload.authentik_sub, + authentik_sub=seed_sub, email=payload.email, display_name=payload.display_name, is_active=payload.is_active, ) authentik_user_id = int(sync["authentik_user_id"]) + if sync.get("authentik_sub"): + resolved_sub = str(sync["authentik_sub"]) + if not resolved_sub: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="authentik_sub_required") row = users_repo.upsert_by_sub( - authentik_sub=payload.authentik_sub, + authentik_sub=resolved_sub, email=payload.email, display_name=payload.display_name, is_active=payload.is_active, diff --git a/backend/app/schemas/catalog.py b/backend/app/schemas/catalog.py index 637f8e8..1242447 100644 --- a/backend/app/schemas/catalog.py +++ b/backend/app/schemas/catalog.py @@ -87,7 +87,7 @@ class MemberItem(BaseModel): class MemberUpsertRequest(BaseModel): - authentik_sub: str + authentik_sub: str | None = None email: str | None = None display_name: str | None = None is_active: bool = True diff --git a/backend/app/services/authentik_admin_service.py b/backend/app/services/authentik_admin_service.py index 793255e..d1dcb9b 100644 --- a/backend/app/services/authentik_admin_service.py +++ b/backend/app/services/authentik_admin_service.py @@ -12,6 +12,7 @@ from app.core.config import Settings class AuthentikSyncResult: user_id: int action: str + authentik_sub: str | None = None class AuthentikAdminService: @@ -66,10 +67,14 @@ class AuthentikAdminService: patch_resp = client.patch(f"/api/v3/core/users/{user_pk}/", json=payload) if patch_resp.status_code >= 400: raise HTTPException(status_code=502, detail="authentik_update_failed") - return AuthentikSyncResult(user_id=user_pk, action="updated") + return AuthentikSyncResult(user_id=user_pk, action="updated", authentik_sub=existing.get("uid")) create_resp = client.post("/api/v3/core/users/", json=payload) if create_resp.status_code >= 400: raise HTTPException(status_code=502, detail="authentik_create_failed") created = create_resp.json() - return AuthentikSyncResult(user_id=int(created["pk"]), action="created") + return AuthentikSyncResult( + user_id=int(created["pk"]), + action="created", + authentik_sub=created.get("uid"), + ) diff --git a/frontend/src/pages/admin/MembersPage.vue b/frontend/src/pages/admin/MembersPage.vue index f98a0ef..79e2ca4 100644 --- a/frontend/src/pages/admin/MembersPage.vue +++ b/frontend/src/pages/admin/MembersPage.vue @@ -28,7 +28,6 @@ - @@ -71,14 +70,13 @@ const showCreateDialog = ref(false) const createFormRef = ref() const creating = ref(false) const createForm = ref({ - authentik_sub: '', email: '', display_name: '', is_active: true, sync_to_authentik: true }) const createRules = { - authentik_sub: [{ required: true, message: '請輸入 Authentik Sub', trigger: 'blur' }] + email: [{ required: true, message: '請輸入 Email', trigger: 'blur' }] } const showEditDialog = ref(false) @@ -107,7 +105,6 @@ async function load() { function resetCreateForm() { createForm.value = { - authentik_sub: '', email: '', display_name: '', is_active: true, diff --git a/frontend/src/pages/admin/ModulesPage.vue b/frontend/src/pages/admin/ModulesPage.vue index 8a5aff0..f913b57 100644 --- a/frontend/src/pages/admin/ModulesPage.vue +++ b/frontend/src/pages/admin/ModulesPage.vue @@ -32,7 +32,9 @@ - + + + @@ -75,8 +77,10 @@ import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { Plus } from '@element-plus/icons-vue' import { getModules, createModule, updateModule } from '@/api/modules' +import { getSystems } from '@/api/systems' const modules = ref([]) +const systems = ref([]) const loading = ref(false) const error = ref(false) const errorMsg = ref('') @@ -98,8 +102,9 @@ async function load() { loading.value = true error.value = false try { - const res = await getModules() - modules.value = res.data?.items || [] + const [modulesRes, systemsRes] = await Promise.all([getModules(), getSystems()]) + modules.value = modulesRes.data?.items || [] + systems.value = systemsRes.data?.items || [] } catch (err) { error.value = true errorMsg.value = err.response?.status === 422 diff --git a/frontend/src/pages/admin/PermissionGroupsPage.vue b/frontend/src/pages/admin/PermissionGroupsPage.vue index 39cd342..51cf4df 100644 --- a/frontend/src/pages/admin/PermissionGroupsPage.vue +++ b/frontend/src/pages/admin/PermissionGroupsPage.vue @@ -36,7 +36,14 @@ - + + + @@ -177,12 +184,14 @@ 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(() => { @@ -190,7 +199,9 @@ const filteredModuleOptions = computed(() => { return modules.value .filter(m => m.system_key === groupPermForm.system && !m.module_key.endsWith('.__system__')) .map(m => ({ - value: m.module_key.split('.', 2)[1] || m.module_key, + value: m.module_key.startsWith(`${groupPermForm.system}.`) + ? m.module_key.slice(groupPermForm.system.length + 1) + : m.module_key, label: `${m.name} (${m.module_key})` })) }) @@ -226,16 +237,18 @@ async function loadGroups() { } async function loadCatalogs() { - const [systemsRes, modulesRes, companiesRes, sitesRes] = await Promise.all([ + const [systemsRes, modulesRes, companiesRes, sitesRes, membersRes] = await Promise.all([ getSystems(), getModules(), getCompanies(), - getSites() + 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 || [] } // Create Group diff --git a/frontend/src/pages/permissions/PermissionAdminPage.vue b/frontend/src/pages/permissions/PermissionAdminPage.vue index 1710cfd..0dbc4ac 100644 --- a/frontend/src/pages/permissions/PermissionAdminPage.vue +++ b/frontend/src/pages/permissions/PermissionAdminPage.vue @@ -199,7 +199,9 @@ const grantModuleOptions = computed(() => { return modules.value .filter(m => m.system_key === grantForm.system && !m.module_key.endsWith('.__system__')) .map(m => ({ - value: m.module_key.split('.', 2)[1] || m.module_key, + value: m.module_key.startsWith(`${grantForm.system}.`) + ? m.module_key.slice(grantForm.system.length + 1) + : m.module_key, label: `${m.name} (${m.module_key})` })) }) @@ -268,7 +270,9 @@ const revokeModuleOptions = computed(() => { return modules.value .filter(m => m.system_key === revokeForm.system && !m.module_key.endsWith('.__system__')) .map(m => ({ - value: m.module_key.split('.', 2)[1] || m.module_key, + value: m.module_key.startsWith(`${revokeForm.system}.`) + ? m.module_key.slice(revokeForm.system.length + 1) + : m.module_key, label: `${m.name} (${m.module_key})` })) })