feat(flow): auto-resolve authentik sub and improve admin dropdown UX
This commit is contained in:
@@ -83,7 +83,11 @@ def _sync_member_to_authentik(
|
|||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
is_active=is_active,
|
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")
|
@router.get("/systems")
|
||||||
@@ -332,17 +336,25 @@ def upsert_member(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> MemberItem:
|
) -> MemberItem:
|
||||||
users_repo = UsersRepository(db)
|
users_repo = UsersRepository(db)
|
||||||
|
resolved_sub = payload.authentik_sub
|
||||||
authentik_user_id = None
|
authentik_user_id = None
|
||||||
if payload.sync_to_authentik:
|
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(
|
sync = _sync_member_to_authentik(
|
||||||
authentik_sub=payload.authentik_sub,
|
authentik_sub=seed_sub,
|
||||||
email=payload.email,
|
email=payload.email,
|
||||||
display_name=payload.display_name,
|
display_name=payload.display_name,
|
||||||
is_active=payload.is_active,
|
is_active=payload.is_active,
|
||||||
)
|
)
|
||||||
authentik_user_id = int(sync["authentik_user_id"])
|
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(
|
row = users_repo.upsert_by_sub(
|
||||||
authentik_sub=payload.authentik_sub,
|
authentik_sub=resolved_sub,
|
||||||
email=payload.email,
|
email=payload.email,
|
||||||
display_name=payload.display_name,
|
display_name=payload.display_name,
|
||||||
is_active=payload.is_active,
|
is_active=payload.is_active,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class MemberItem(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class MemberUpsertRequest(BaseModel):
|
class MemberUpsertRequest(BaseModel):
|
||||||
authentik_sub: str
|
authentik_sub: str | None = None
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
display_name: str | None = None
|
display_name: str | None = None
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from app.core.config import Settings
|
|||||||
class AuthentikSyncResult:
|
class AuthentikSyncResult:
|
||||||
user_id: int
|
user_id: int
|
||||||
action: str
|
action: str
|
||||||
|
authentik_sub: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuthentikAdminService:
|
class AuthentikAdminService:
|
||||||
@@ -66,10 +67,14 @@ class AuthentikAdminService:
|
|||||||
patch_resp = client.patch(f"/api/v3/core/users/{user_pk}/", json=payload)
|
patch_resp = client.patch(f"/api/v3/core/users/{user_pk}/", json=payload)
|
||||||
if patch_resp.status_code >= 400:
|
if patch_resp.status_code >= 400:
|
||||||
raise HTTPException(status_code=502, detail="authentik_update_failed")
|
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)
|
create_resp = client.post("/api/v3/core/users/", json=payload)
|
||||||
if create_resp.status_code >= 400:
|
if create_resp.status_code >= 400:
|
||||||
raise HTTPException(status_code=502, detail="authentik_create_failed")
|
raise HTTPException(status_code=502, detail="authentik_create_failed")
|
||||||
created = create_resp.json()
|
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"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
<el-dialog v-model="showCreateDialog" title="新增會員" @close="resetCreateForm">
|
<el-dialog v-model="showCreateDialog" title="新增會員" @close="resetCreateForm">
|
||||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||||
<el-form-item label="Authentik Sub" prop="authentik_sub"><el-input v-model="createForm.authentik_sub" /></el-form-item>
|
|
||||||
<el-form-item label="Email" prop="email"><el-input v-model="createForm.email" /></el-form-item>
|
<el-form-item label="Email" prop="email"><el-input v-model="createForm.email" /></el-form-item>
|
||||||
<el-form-item label="顯示名稱" prop="display_name"><el-input v-model="createForm.display_name" /></el-form-item>
|
<el-form-item label="顯示名稱" prop="display_name"><el-input v-model="createForm.display_name" /></el-form-item>
|
||||||
<el-form-item label="啟用"><el-switch v-model="createForm.is_active" /></el-form-item>
|
<el-form-item label="啟用"><el-switch v-model="createForm.is_active" /></el-form-item>
|
||||||
@@ -71,14 +70,13 @@ const showCreateDialog = ref(false)
|
|||||||
const createFormRef = ref()
|
const createFormRef = ref()
|
||||||
const creating = ref(false)
|
const creating = ref(false)
|
||||||
const createForm = ref({
|
const createForm = ref({
|
||||||
authentik_sub: '',
|
|
||||||
email: '',
|
email: '',
|
||||||
display_name: '',
|
display_name: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
sync_to_authentik: true
|
sync_to_authentik: true
|
||||||
})
|
})
|
||||||
const createRules = {
|
const createRules = {
|
||||||
authentik_sub: [{ required: true, message: '請輸入 Authentik Sub', trigger: 'blur' }]
|
email: [{ required: true, message: '請輸入 Email', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const showEditDialog = ref(false)
|
const showEditDialog = ref(false)
|
||||||
@@ -107,7 +105,6 @@ async function load() {
|
|||||||
|
|
||||||
function resetCreateForm() {
|
function resetCreateForm() {
|
||||||
createForm.value = {
|
createForm.value = {
|
||||||
authentik_sub: '',
|
|
||||||
email: '',
|
email: '',
|
||||||
display_name: '',
|
display_name: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
<el-dialog v-model="showDialog" title="新增模組" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增模組" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<el-form-item label="System Key" prop="system_key">
|
<el-form-item label="System Key" prop="system_key">
|
||||||
<el-input v-model="form.system_key" placeholder="mkt" />
|
<el-select v-model="form.system_key" 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>
|
||||||
<el-form-item label="Module Key" prop="module_key">
|
<el-form-item label="Module Key" prop="module_key">
|
||||||
<el-input v-model="form.module_key" placeholder="campaign" />
|
<el-input v-model="form.module_key" placeholder="campaign" />
|
||||||
@@ -75,8 +77,10 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import { getModules, createModule, updateModule } from '@/api/modules'
|
import { getModules, createModule, updateModule } from '@/api/modules'
|
||||||
|
import { getSystems } from '@/api/systems'
|
||||||
|
|
||||||
const modules = ref([])
|
const modules = ref([])
|
||||||
|
const systems = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(false)
|
const error = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
@@ -98,8 +102,9 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = false
|
error.value = false
|
||||||
try {
|
try {
|
||||||
const res = await getModules()
|
const [modulesRes, systemsRes] = await Promise.all([getModules(), getSystems()])
|
||||||
modules.value = res.data?.items || []
|
modules.value = modulesRes.data?.items || []
|
||||||
|
systems.value = systemsRes.data?.items || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = true
|
error.value = true
|
||||||
errorMsg.value = err.response?.status === 422
|
errorMsg.value = err.response?.status === 422
|
||||||
|
|||||||
@@ -36,7 +36,14 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Authentik Sub">
|
<el-form-item label="Authentik Sub">
|
||||||
<el-input v-model="memberForm.authentikSub" placeholder="authentik-sub-xxx" />
|
<el-select v-model="memberForm.authentikSub" placeholder="選擇會員" filterable allow-create default-first-option 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>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :loading="addingMember" @click="handleAddMember" :disabled="!memberForm.groupKey || !memberForm.authentikSub">
|
<el-button type="primary" :loading="addingMember" @click="handleAddMember" :disabled="!memberForm.groupKey || !memberForm.authentikSub">
|
||||||
@@ -177,12 +184,14 @@ import { getSystems } from '@/api/systems'
|
|||||||
import { getModules } from '@/api/modules'
|
import { getModules } from '@/api/modules'
|
||||||
import { getCompanies } from '@/api/companies'
|
import { getCompanies } from '@/api/companies'
|
||||||
import { getSites } from '@/api/sites'
|
import { getSites } from '@/api/sites'
|
||||||
|
import { getMembers } from '@/api/members'
|
||||||
|
|
||||||
const activeTab = ref('groups')
|
const activeTab = ref('groups')
|
||||||
const systems = ref([])
|
const systems = ref([])
|
||||||
const modules = ref([])
|
const modules = ref([])
|
||||||
const companies = ref([])
|
const companies = ref([])
|
||||||
const sites = ref([])
|
const sites = ref([])
|
||||||
|
const members = ref([])
|
||||||
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
||||||
|
|
||||||
const filteredModuleOptions = computed(() => {
|
const filteredModuleOptions = computed(() => {
|
||||||
@@ -190,7 +199,9 @@ const filteredModuleOptions = computed(() => {
|
|||||||
return modules.value
|
return modules.value
|
||||||
.filter(m => m.system_key === groupPermForm.system && !m.module_key.endsWith('.__system__'))
|
.filter(m => m.system_key === groupPermForm.system && !m.module_key.endsWith('.__system__'))
|
||||||
.map(m => ({
|
.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})`
|
label: `${m.name} (${m.module_key})`
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -226,16 +237,18 @@ async function loadGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCatalogs() {
|
async function loadCatalogs() {
|
||||||
const [systemsRes, modulesRes, companiesRes, sitesRes] = await Promise.all([
|
const [systemsRes, modulesRes, companiesRes, sitesRes, membersRes] = await Promise.all([
|
||||||
getSystems(),
|
getSystems(),
|
||||||
getModules(),
|
getModules(),
|
||||||
getCompanies(),
|
getCompanies(),
|
||||||
getSites()
|
getSites(),
|
||||||
|
getMembers()
|
||||||
])
|
])
|
||||||
systems.value = systemsRes.data?.items || []
|
systems.value = systemsRes.data?.items || []
|
||||||
modules.value = modulesRes.data?.items || []
|
modules.value = modulesRes.data?.items || []
|
||||||
companies.value = companiesRes.data?.items || []
|
companies.value = companiesRes.data?.items || []
|
||||||
sites.value = sitesRes.data?.items || []
|
sites.value = sitesRes.data?.items || []
|
||||||
|
members.value = membersRes.data?.items || []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Group
|
// Create Group
|
||||||
|
|||||||
@@ -199,7 +199,9 @@ const grantModuleOptions = computed(() => {
|
|||||||
return modules.value
|
return modules.value
|
||||||
.filter(m => m.system_key === grantForm.system && !m.module_key.endsWith('.__system__'))
|
.filter(m => m.system_key === grantForm.system && !m.module_key.endsWith('.__system__'))
|
||||||
.map(m => ({
|
.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})`
|
label: `${m.name} (${m.module_key})`
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -268,7 +270,9 @@ const revokeModuleOptions = computed(() => {
|
|||||||
return modules.value
|
return modules.value
|
||||||
.filter(m => m.system_key === revokeForm.system && !m.module_key.endsWith('.__system__'))
|
.filter(m => m.system_key === revokeForm.system && !m.module_key.endsWith('.__system__'))
|
||||||
.map(m => ({
|
.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})`
|
label: `${m.name} (${m.module_key})`
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user