feat(admin): add delete APIs and UI actions for all admin resources

This commit is contained in:
Chris
2026-03-31 20:58:20 +08:00
parent c4492a3072
commit f6f86d4bfb
15 changed files with 245 additions and 15 deletions

View File

@@ -1,12 +1,14 @@
import secrets
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from app.core.keygen import generate_key
from app.core.config import get_settings
from app.db.session import get_db
from app.models.api_client import ApiClient
from app.models.permission_group_member import PermissionGroupMember
from app.models.permission_group_permission import PermissionGroupPermission
from app.repositories.companies_repo import CompaniesRepository
from app.repositories.modules_repo import ModulesRepository
from app.repositories.permission_groups_repo import PermissionGroupsRepository
@@ -195,6 +197,21 @@ def update_system(
return SystemItem(id=row.id, system_key=row.system_key, name=row.name, status=row.status)
@router.delete("/systems/{system_key}")
def delete_system(
system_key: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
repo = SystemsRepository(db)
row = repo.get_by_key(system_key)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
db.execute(delete(PermissionGroupPermission).where(PermissionGroupPermission.system == system_key))
db.delete(row)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.get("/modules")
def list_modules(
db: Session = Depends(get_db),
@@ -253,6 +270,21 @@ def update_module(
return ModuleItem(id=row.id, system_key=row.system_key, module_key=row.module_key, name=row.name, status=row.status)
@router.delete("/modules/{module_key}")
def delete_module(
module_key: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
modules_repo = ModulesRepository(db)
row = modules_repo.get_by_key(module_key)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found")
db.execute(delete(PermissionGroupPermission).where(PermissionGroupPermission.module == module_key))
db.delete(row)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.get("/systems/{system_key}/groups")
def list_system_groups(
system_key: str,
@@ -374,6 +406,30 @@ def update_company(
return CompanyItem(id=row.id, company_key=row.company_key, name=row.name, status=row.status)
@router.delete("/companies/{company_key}")
def delete_company(
company_key: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db)
company = companies_repo.get_by_key(company_key)
if not company:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
company_sites, _ = sites_repo.list(company_id=company.id, limit=10000, offset=0)
company_site_keys = [s.site_key for s in company_sites]
if company_site_keys:
db.execute(
delete(PermissionGroupPermission).where(
PermissionGroupPermission.scope_type == "site",
PermissionGroupPermission.scope_id.in_(company_site_keys),
)
)
db.delete(company)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.get("/companies/{company_key}/sites")
def list_company_sites(
company_key: str,
@@ -478,6 +534,26 @@ def update_site(
return SiteItem(id=row.id, site_key=row.site_key, company_key=company_key, name=row.name, status=row.status)
@router.delete("/sites/{site_key}")
def delete_site(
site_key: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
sites_repo = SitesRepository(db)
row = sites_repo.get_by_key(site_key)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="site_not_found")
db.execute(
delete(PermissionGroupPermission).where(
PermissionGroupPermission.scope_type == "site",
PermissionGroupPermission.scope_id == site_key,
)
)
db.delete(row)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.get("/members")
def list_members(
db: Session = Depends(get_db),
@@ -595,6 +671,21 @@ def update_member(
)
@router.delete("/members/{authentik_sub}")
def delete_member(
authentik_sub: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
users_repo = UsersRepository(db)
row = users_repo.get_by_sub(authentik_sub)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.authentik_sub == authentik_sub))
db.delete(row)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.post("/members/{authentik_sub}/password/reset", response_model=MemberPasswordResetResponse)
def reset_member_password(
authentik_sub: str,
@@ -759,6 +850,19 @@ def rotate_api_client_key(
return ApiClientRotateKeyResponse(client_key=row.client_key, api_key=api_key)
@router.delete("/api-clients/{client_key}")
def delete_api_client(
client_key: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
row = db.scalar(select(ApiClient).where(ApiClient.client_key == client_key))
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="api_client_not_found")
db.delete(row)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.get("/permission-groups")
def list_permission_groups(
db: Session = Depends(get_db),
@@ -898,6 +1002,20 @@ def update_permission_group(
return PermissionGroupItem(id=row.id, group_key=row.group_key, name=row.name, status=row.status)
@router.delete("/permission-groups/{group_key}")
def delete_permission_group(
group_key: str,
db: Session = Depends(get_db),
) -> dict[str, int | str]:
repo = PermissionGroupsRepository(db)
row = repo.get_by_key(group_key)
if not row:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
db.delete(row)
db.commit()
return {"deleted": 1, "result": "deleted"}
@router.post("/permission-groups/{group_key}/members/{authentik_sub}")
def add_group_member(
group_key: str,

View File

@@ -4,3 +4,4 @@ export const getApiClients = (params) => adminHttp.get('/admin/api-clients', { p
export const createApiClient = (data) => adminHttp.post('/admin/api-clients', data)
export const updateApiClient = (clientKey, data) => adminHttp.patch(`/admin/api-clients/${clientKey}`, data)
export const rotateApiClientKey = (clientKey) => adminHttp.post(`/admin/api-clients/${clientKey}/rotate-key`)
export const deleteApiClient = (clientKey) => adminHttp.delete(`/admin/api-clients/${clientKey}`)

View File

@@ -3,4 +3,5 @@ import { adminHttp } from './http'
export const getCompanies = () => adminHttp.get('/admin/companies')
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
export const deleteCompany = (companyKey) => adminHttp.delete(`/admin/companies/${companyKey}`)
export const getCompanySites = (companyKey) => adminHttp.get(`/admin/companies/${companyKey}/sites`)

View File

@@ -3,6 +3,7 @@ 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 deleteMember = (authentikSub) => adminHttp.delete(`/admin/members/${authentikSub}`)
export const resetMemberPassword = (authentikSub) => adminHttp.post(`/admin/members/${authentikSub}/password/reset`)
export const getMemberPermissionGroups = (authentikSub) => adminHttp.get(`/admin/members/${authentikSub}/permission-groups`)
export const setMemberPermissionGroups = (authentikSub, groupKeys) =>

View File

@@ -3,5 +3,6 @@ import { adminHttp } from './http'
export const getModules = () => adminHttp.get('/admin/modules')
export const createModule = (data) => adminHttp.post('/admin/modules', data)
export const updateModule = (moduleKey, data) => adminHttp.patch(`/admin/modules/${moduleKey}`, data)
export const deleteModule = (moduleKey) => adminHttp.delete(`/admin/modules/${moduleKey}`)
export const getModuleGroups = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/groups`)
export const getModuleMembers = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/members`)

View File

@@ -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 deletePermissionGroup = (groupKey) => adminHttp.delete(`/admin/permission-groups/${groupKey}`)
export const getPermissionGroupPermissions = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/permissions`)
export const getPermissionGroupBindings = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/bindings`)
export const updatePermissionGroupBindings = (groupKey, data) =>

View File

@@ -3,3 +3,4 @@ import { adminHttp } from './http'
export const getSites = () => adminHttp.get('/admin/sites')
export const createSite = (data) => adminHttp.post('/admin/sites', data)
export const updateSite = (siteKey, data) => adminHttp.patch(`/admin/sites/${siteKey}`, data)
export const deleteSite = (siteKey) => adminHttp.delete(`/admin/sites/${siteKey}`)

View File

@@ -3,5 +3,6 @@ import { adminHttp } from './http'
export const getSystems = () => adminHttp.get('/admin/systems')
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
export const deleteSystem = (systemKey) => adminHttp.delete(`/admin/systems/${systemKey}`)
export const getSystemGroups = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/groups`)
export const getSystemMembers = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/members`)

View File

@@ -31,6 +31,7 @@
<el-button size="small" :type="row.status === 'active' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === 'active' ? '停用' : '啟用' }}
</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -101,7 +102,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { createApiClient, getApiClients, rotateApiClientKey, updateApiClient } from '@/api/api-clients'
import { createApiClient, getApiClients, rotateApiClientKey, updateApiClient, deleteApiClient } from '@/api/api-clients'
const items = ref([])
const loading = ref(false)
@@ -278,5 +279,17 @@ async function toggleStatus(row) {
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`確認刪除 API Client ${row.name}${row.client_key}`, '刪除確認', { type: 'warning' })
await deleteApiClient(row.client_key)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
onMounted(load)
</script>

View File

@@ -13,10 +13,11 @@
<el-table-column prop="company_key" label="Company Key" width="220" />
<el-table-column prop="name" label="名稱" min-width="200" />
<el-table-column prop="status" label="狀態" width="120" />
<el-table-column label="操作" width="200">
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" @click="openSites(row)">站台</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -64,9 +65,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getCompanies, createCompany, updateCompany, getCompanySites } from '@/api/companies'
import { getCompanies, createCompany, updateCompany, deleteCompany, getCompanySites } from '@/api/companies'
const companies = ref([])
const loading = ref(false)
@@ -163,5 +164,21 @@ async function openSites(row) {
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`確認刪除公司 ${row.name}${row.company_key})?此操作會一併刪除旗下站台與相關授權。`,
'刪除確認',
{ type: 'warning' }
)
await deleteCompany(row.company_key)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
onMounted(load)
</script>

View File

@@ -24,6 +24,7 @@
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" type="warning" @click="handleResetPassword(row)">重設密碼</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -71,12 +72,13 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import {
getMembers,
upsertMember,
updateMember,
deleteMember,
resetMemberPassword,
getMemberPermissionGroups,
setMemberPermissionGroups
@@ -234,5 +236,21 @@ async function handleResetPassword(row) {
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`確認刪除會員 ${row.display_name || row.email || row.username || row.authentik_sub}`,
'刪除確認',
{ type: 'warning' }
)
await deleteMember(row.authentik_sub)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
onMounted(load)
</script>

View File

@@ -29,6 +29,7 @@
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" @click="openRelations(row, 'groups')">群組</el-button>
<el-button size="small" @click="openRelations(row, 'members')">會員</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -102,9 +103,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getModules, createModule, updateModule, getModuleGroups, getModuleMembers } from '@/api/modules'
import { getModules, createModule, updateModule, deleteModule, getModuleGroups, getModuleMembers } from '@/api/modules'
import { getSystems } from '@/api/systems'
const modules = ref([])
@@ -200,6 +201,18 @@ async function handleEdit() {
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`確認刪除模組 ${row.name}${row.module_key}`, '刪除確認', { type: 'warning' })
await deleteModule(row.module_key)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
async function openRelations(row, tab) {
relationModuleKey.value = row.module_key
relationTab.value = tab

View File

@@ -17,10 +17,11 @@
<el-table-column prop="group_key" label="Group Key" width="180" />
<el-table-column prop="name" label="群組名稱" min-width="220" />
<el-table-column prop="status" label="狀態" width="120" />
<el-table-column label="操作" width="220">
<el-table-column label="操作" width="300">
<template #default="{ row }">
<el-button size="small" @click="openEditGroup(row)">編輯</el-button>
<el-button size="small" @click="openBindingDialog(row)">設定關聯</el-button>
<el-button size="small" type="danger" @click="handleDeleteGroup(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -128,12 +129,13 @@
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
getPermissionGroups,
createPermissionGroup,
updatePermissionGroup,
deletePermissionGroup,
getPermissionGroupPermissions,
getPermissionGroupBindings,
updatePermissionGroupBindings
@@ -283,6 +285,18 @@ async function handleEditGroup() {
}
}
async function handleDeleteGroup(row) {
try {
await ElMessageBox.confirm(`確認刪除群組 ${row.name}${row.group_key}`, '刪除確認', { type: 'warning' })
await deletePermissionGroup(row.group_key)
ElMessage.success('刪除成功')
await loadGroups()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
async function openBindingDialog(row) {
bindingGroupKey.value = row.group_key
showBindingDialog.value = true

View File

@@ -16,9 +16,10 @@
</el-table-column>
<el-table-column prop="name" label="名稱" min-width="180" />
<el-table-column prop="status" label="狀態" width="120" />
<el-table-column label="操作" width="120">
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -64,9 +65,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getSites, createSite, updateSite } from '@/api/sites'
import { getSites, createSite, updateSite, deleteSite } from '@/api/sites'
import { getCompanies } from '@/api/companies'
const sites = ref([])
@@ -166,5 +167,21 @@ async function handleEdit() {
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`確認刪除站台 ${row.name}${row.site_key})?此操作會清理相關授權。`,
'刪除確認',
{ type: 'warning' }
)
await deleteSite(row.site_key)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
onMounted(load)
</script>

View File

@@ -26,6 +26,7 @@
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" @click="openRelations(row, 'groups')">群組</el-button>
<el-button size="small" @click="openRelations(row, 'members')">會員</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
@@ -94,9 +95,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getSystems, createSystem, updateSystem, getSystemGroups, getSystemMembers } from '@/api/systems'
import { getSystems, createSystem, updateSystem, deleteSystem, getSystemGroups, getSystemMembers } from '@/api/systems'
const systems = ref([])
const loading = ref(false)
@@ -188,6 +189,18 @@ async function handleEdit() {
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`確認刪除系統 ${row.name}${row.system_key}`, '刪除確認', { type: 'warning' })
await deleteSystem(row.system_key)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除失敗')
}
}
async function openRelations(row, tab) {
relationSystemKey.value = row.system_key
relationTab.value = tab