feat(flow): unify member-group-permission admin workflow and docs
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
@@ -9,7 +11,12 @@ from app.repositories.permissions_repo import PermissionsRepository
|
||||
from app.repositories.sites_repo import SitesRepository
|
||||
from app.repositories.systems_repo import SystemsRepository
|
||||
from app.repositories.users_repo import UsersRepository
|
||||
from app.schemas.permissions import PermissionGrantRequest, PermissionRevokeRequest
|
||||
from app.schemas.permissions import (
|
||||
DirectPermissionListResponse,
|
||||
DirectPermissionRow,
|
||||
PermissionGrantRequest,
|
||||
PermissionRevokeRequest,
|
||||
)
|
||||
from app.security.api_client_auth import require_api_client
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
@@ -98,3 +105,42 @@ def revoke_permission(
|
||||
site_id=site_id,
|
||||
)
|
||||
return {"deleted": deleted, "result": "revoked"}
|
||||
|
||||
|
||||
@router.get("/permissions/direct", response_model=DirectPermissionListResponse)
|
||||
def list_direct_permissions(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
keyword: str | None = Query(default=None),
|
||||
scope_type: str | None = Query(default=None),
|
||||
limit: int = Query(default=200, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> DirectPermissionListResponse:
|
||||
perms_repo = PermissionsRepository(db)
|
||||
items, total = perms_repo.list_direct_permissions(
|
||||
keyword=keyword,
|
||||
scope_type=scope_type,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return DirectPermissionListResponse(
|
||||
items=[DirectPermissionRow(**item) for item in items],
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/permissions/direct/{permission_id}")
|
||||
def delete_direct_permission(
|
||||
permission_id: str,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, int | str]:
|
||||
try:
|
||||
normalized_permission_id = str(UUID(permission_id))
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid_permission_id")
|
||||
perms_repo = PermissionsRepository(db)
|
||||
deleted = perms_repo.revoke_by_permission_id(normalized_permission_id)
|
||||
return {"deleted": deleted, "result": "revoked"}
|
||||
|
||||
@@ -15,6 +15,8 @@ from app.schemas.catalog import (
|
||||
CompanyItem,
|
||||
CompanyUpdateRequest,
|
||||
MemberItem,
|
||||
MemberPermissionGroupsResponse,
|
||||
MemberPermissionGroupsUpdateRequest,
|
||||
MemberUpdateRequest,
|
||||
MemberUpsertRequest,
|
||||
ModuleCreateRequest,
|
||||
@@ -22,6 +24,7 @@ from app.schemas.catalog import (
|
||||
ModuleUpdateRequest,
|
||||
PermissionGroupCreateRequest,
|
||||
PermissionGroupItem,
|
||||
PermissionGroupPermissionItem,
|
||||
PermissionGroupUpdateRequest,
|
||||
SiteCreateRequest,
|
||||
SiteItem,
|
||||
@@ -411,6 +414,45 @@ def update_member(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/members/{authentik_sub}/permission-groups", response_model=MemberPermissionGroupsResponse)
|
||||
def get_member_permission_groups(
|
||||
authentik_sub: str,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> MemberPermissionGroupsResponse:
|
||||
users_repo = UsersRepository(db)
|
||||
groups_repo = PermissionGroupsRepository(db)
|
||||
user = users_repo.get_by_sub(authentik_sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
|
||||
group_keys = groups_repo.list_group_keys_by_member_sub(authentik_sub)
|
||||
return MemberPermissionGroupsResponse(authentik_sub=authentik_sub, group_keys=group_keys)
|
||||
|
||||
|
||||
@router.put("/members/{authentik_sub}/permission-groups", response_model=MemberPermissionGroupsResponse)
|
||||
def set_member_permission_groups(
|
||||
authentik_sub: str,
|
||||
payload: MemberPermissionGroupsUpdateRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> MemberPermissionGroupsResponse:
|
||||
users_repo = UsersRepository(db)
|
||||
groups_repo = PermissionGroupsRepository(db)
|
||||
user = users_repo.get_by_sub(authentik_sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
|
||||
|
||||
unique_group_keys = list(dict.fromkeys(payload.group_keys))
|
||||
groups = groups_repo.get_by_keys(unique_group_keys)
|
||||
found_keys = {g.group_key for g in groups}
|
||||
missing = [k for k in unique_group_keys if k not in found_keys]
|
||||
if missing:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"group_not_found:{','.join(missing)}")
|
||||
|
||||
groups_repo.replace_member_groups(authentik_sub, [g.id for g in groups])
|
||||
return MemberPermissionGroupsResponse(authentik_sub=authentik_sub, group_keys=unique_group_keys)
|
||||
|
||||
|
||||
@router.get("/permission-groups")
|
||||
def list_permission_groups(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
@@ -423,6 +465,32 @@ def list_permission_groups(
|
||||
return {"items": [PermissionGroupItem(id=i.id, group_key=i.group_key, name=i.name, status=i.status).model_dump() for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.get("/permission-groups/{group_key}/permissions")
|
||||
def list_permission_group_permissions(
|
||||
group_key: str,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, list[dict]]:
|
||||
repo = PermissionGroupsRepository(db)
|
||||
group = repo.get_by_key(group_key)
|
||||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||
rows = repo.list_group_permissions(group.id)
|
||||
return {
|
||||
"items": [
|
||||
PermissionGroupPermissionItem(
|
||||
id=r.id,
|
||||
system=r.system,
|
||||
module=r.module,
|
||||
action=r.action,
|
||||
scope_type=r.scope_type,
|
||||
scope_id=r.scope_id,
|
||||
).model_dump()
|
||||
for r in rows
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/permission-groups", response_model=PermissionGroupItem)
|
||||
def create_permission_group(
|
||||
payload: PermissionGroupCreateRequest,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -16,6 +18,12 @@ class PermissionGroupsRepository:
|
||||
def get_by_id(self, group_id: str) -> PermissionGroup | None:
|
||||
return self.db.scalar(select(PermissionGroup).where(PermissionGroup.id == group_id))
|
||||
|
||||
def get_by_keys(self, group_keys: list[str]) -> list[PermissionGroup]:
|
||||
if not group_keys:
|
||||
return []
|
||||
stmt = select(PermissionGroup).where(PermissionGroup.group_key.in_(group_keys))
|
||||
return list(self.db.scalars(stmt).all())
|
||||
|
||||
def list(self, limit: int = 100, offset: int = 0) -> tuple[list[PermissionGroup], int]:
|
||||
stmt = select(PermissionGroup).order_by(PermissionGroup.created_at.desc()).limit(limit).offset(offset)
|
||||
count_stmt = select(func.count()).select_from(PermissionGroup)
|
||||
@@ -60,6 +68,22 @@ class PermissionGroupsRepository:
|
||||
self.db.commit()
|
||||
return int(result.rowcount or 0)
|
||||
|
||||
def list_group_keys_by_member_sub(self, authentik_sub: str) -> list[str]:
|
||||
stmt = (
|
||||
select(PermissionGroup.group_key)
|
||||
.select_from(PermissionGroupMember)
|
||||
.join(PermissionGroup, PermissionGroup.id == PermissionGroupMember.group_id)
|
||||
.where(PermissionGroupMember.authentik_sub == authentik_sub)
|
||||
.order_by(PermissionGroup.group_key.asc())
|
||||
)
|
||||
return [row[0] for row in self.db.execute(stmt).all()]
|
||||
|
||||
def replace_member_groups(self, authentik_sub: str, group_ids: list[str]) -> None:
|
||||
self.db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.authentik_sub == authentik_sub))
|
||||
for group_id in group_ids:
|
||||
self.db.add(PermissionGroupMember(group_id=group_id, authentik_sub=authentik_sub))
|
||||
self.db.commit()
|
||||
|
||||
def grant_group_permission(
|
||||
self,
|
||||
group_id: str,
|
||||
@@ -93,6 +117,14 @@ class PermissionGroupsRepository:
|
||||
self.db.refresh(row)
|
||||
return row
|
||||
|
||||
def list_group_permissions(self, group_id: str) -> list[PermissionGroupPermission]:
|
||||
stmt = (
|
||||
select(PermissionGroupPermission)
|
||||
.where(PermissionGroupPermission.group_id == group_id)
|
||||
.order_by(PermissionGroupPermission.scope_type.asc(), PermissionGroupPermission.scope_id.asc(), PermissionGroupPermission.system.asc(), PermissionGroupPermission.module.asc(), PermissionGroupPermission.action.asc())
|
||||
)
|
||||
return list(self.db.scalars(stmt).all())
|
||||
|
||||
def revoke_group_permission(
|
||||
self,
|
||||
group_id: str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import and_, delete, literal, or_, select
|
||||
from sqlalchemy import and_, delete, func, literal, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.company import Company
|
||||
@@ -6,6 +6,7 @@ from app.models.module import Module
|
||||
from app.models.permission_group_member import PermissionGroupMember
|
||||
from app.models.permission_group_permission import PermissionGroupPermission
|
||||
from app.models.site import Site
|
||||
from app.models.user import User
|
||||
from app.models.user_scope_permission import UserScopePermission
|
||||
|
||||
|
||||
@@ -119,3 +120,101 @@ class PermissionsRepository:
|
||||
result = self.db.execute(stmt)
|
||||
self.db.commit()
|
||||
return int(result.rowcount or 0)
|
||||
|
||||
def list_direct_permissions(
|
||||
self,
|
||||
*,
|
||||
keyword: str | None = None,
|
||||
scope_type: str | None = None,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[dict], int]:
|
||||
stmt = (
|
||||
select(
|
||||
UserScopePermission.id,
|
||||
User.authentik_sub,
|
||||
User.email,
|
||||
User.display_name,
|
||||
UserScopePermission.scope_type,
|
||||
Company.company_key,
|
||||
Site.site_key,
|
||||
Module.module_key,
|
||||
UserScopePermission.action,
|
||||
UserScopePermission.created_at,
|
||||
)
|
||||
.select_from(UserScopePermission)
|
||||
.join(User, User.id == UserScopePermission.user_id)
|
||||
.join(Module, Module.id == UserScopePermission.module_id)
|
||||
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
||||
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
||||
)
|
||||
count_stmt = (
|
||||
select(func.count())
|
||||
.select_from(UserScopePermission)
|
||||
.join(User, User.id == UserScopePermission.user_id)
|
||||
.join(Module, Module.id == UserScopePermission.module_id)
|
||||
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
||||
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
||||
)
|
||||
|
||||
if scope_type in {"company", "site"}:
|
||||
stmt = stmt.where(UserScopePermission.scope_type == scope_type)
|
||||
count_stmt = count_stmt.where(UserScopePermission.scope_type == scope_type)
|
||||
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
cond = or_(
|
||||
User.authentik_sub.ilike(pattern),
|
||||
User.email.ilike(pattern),
|
||||
User.display_name.ilike(pattern),
|
||||
Module.module_key.ilike(pattern),
|
||||
Company.company_key.ilike(pattern),
|
||||
Site.site_key.ilike(pattern),
|
||||
UserScopePermission.action.ilike(pattern),
|
||||
)
|
||||
stmt = stmt.where(cond)
|
||||
count_stmt = count_stmt.where(cond)
|
||||
|
||||
stmt = stmt.order_by(UserScopePermission.created_at.desc()).limit(limit).offset(offset)
|
||||
rows = self.db.execute(stmt).all()
|
||||
total = int(self.db.scalar(count_stmt) or 0)
|
||||
items: list[dict] = []
|
||||
for row in rows:
|
||||
(
|
||||
permission_id,
|
||||
authentik_sub,
|
||||
email,
|
||||
display_name,
|
||||
row_scope_type,
|
||||
company_key,
|
||||
site_key,
|
||||
module_key,
|
||||
action,
|
||||
created_at,
|
||||
) = row
|
||||
scope_id = company_key if row_scope_type == "company" else site_key
|
||||
system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None
|
||||
module_name = module_key.split(".", 1)[1] if isinstance(module_key, str) and "." in module_key else module_key
|
||||
if module_name == "__system__":
|
||||
module_name = None
|
||||
items.append(
|
||||
{
|
||||
"permission_id": permission_id,
|
||||
"authentik_sub": authentik_sub,
|
||||
"email": email,
|
||||
"display_name": display_name,
|
||||
"scope_type": row_scope_type,
|
||||
"scope_id": scope_id,
|
||||
"system": system_key,
|
||||
"module": module_name,
|
||||
"action": action,
|
||||
"created_at": created_at,
|
||||
}
|
||||
)
|
||||
return items, total
|
||||
|
||||
def revoke_by_permission_id(self, permission_id: str) -> int:
|
||||
stmt = delete(UserScopePermission).where(UserScopePermission.id == permission_id)
|
||||
result = self.db.execute(stmt)
|
||||
self.db.commit()
|
||||
return int(result.rowcount or 0)
|
||||
|
||||
@@ -101,6 +101,10 @@ class MemberUpdateRequest(BaseModel):
|
||||
sync_to_authentik: bool = True
|
||||
|
||||
|
||||
class MemberPermissionGroupsUpdateRequest(BaseModel):
|
||||
group_keys: list[str]
|
||||
|
||||
|
||||
class ListResponse(BaseModel):
|
||||
items: list
|
||||
total: int
|
||||
@@ -124,3 +128,17 @@ class PermissionGroupItem(BaseModel):
|
||||
group_key: str
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class PermissionGroupPermissionItem(BaseModel):
|
||||
id: str
|
||||
system: str
|
||||
module: str
|
||||
action: str
|
||||
scope_type: str
|
||||
scope_id: str
|
||||
|
||||
|
||||
class MemberPermissionGroupsResponse(BaseModel):
|
||||
authentik_sub: str
|
||||
group_keys: list[str]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -32,3 +34,23 @@ class PermissionItem(BaseModel):
|
||||
class PermissionSnapshotResponse(BaseModel):
|
||||
authentik_sub: str
|
||||
permissions: list[PermissionItem]
|
||||
|
||||
|
||||
class DirectPermissionRow(BaseModel):
|
||||
permission_id: str
|
||||
authentik_sub: str
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
scope_type: str
|
||||
scope_id: str
|
||||
system: str | None = None
|
||||
module: str | None = None
|
||||
action: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class DirectPermissionListResponse(BaseModel):
|
||||
items: list[DirectPermissionRow]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
# memberapi.ose.tw 後端架構(公司/品牌站台/會員)
|
||||
# memberapi.ose.tw 後端架構(公司/品牌站台/會員 + 系統/模組權限)
|
||||
|
||||
## 核心主檔(對齊 DB Schema)
|
||||
- `users`:會員
|
||||
- `companies`:公司
|
||||
- `sites`:品牌站台(隸屬 company)
|
||||
- `systems`:系統層(member/mkt/...)
|
||||
- `modules`:模組(使用 `system.module` key)
|
||||
## 資料層級
|
||||
- 業務層級:`companies -> sites -> users`
|
||||
- 功能層級:`systems -> modules`
|
||||
- 授權掛載點:
|
||||
- Scope:`company` 或 `site`
|
||||
- 能力:`system` 必填,`module` 選填(空值代表系統層)
|
||||
|
||||
## 權限模型
|
||||
- 直接權限:`user_scope_permissions`
|
||||
- 群組權限:`permission_groups` + `permission_group_members` + `permission_group_permissions`
|
||||
- Snapshot 回傳:合併「user 直接 + group」去重
|
||||
- 直接授權:`user_scope_permissions`
|
||||
- 群組授權:`permission_groups` + `permission_group_members` + `permission_group_permissions`
|
||||
- 權限快照:`/me/permissions/snapshot` 會合併「直接 + 群組」並去重
|
||||
|
||||
## 授權層級
|
||||
- `system` 必填
|
||||
- `module` 選填
|
||||
- 有值:`{system}.{module}`(例:`mkt.campaign`)
|
||||
- 無值:系統層權限,使用 `system.__system__`
|
||||
## 目前後端 API(管理面)
|
||||
- 主資料:
|
||||
- `GET|POST|PATCH /admin/systems`
|
||||
- `GET|POST|PATCH /admin/modules`
|
||||
- `GET|POST|PATCH /admin/companies`
|
||||
- `GET|POST|PATCH /admin/sites`
|
||||
- 會員:
|
||||
- `GET /admin/members`
|
||||
- `POST /admin/members/upsert`
|
||||
- `PATCH /admin/members/{authentik_sub}`
|
||||
- 會員群組(改由會員頁管理):
|
||||
- `GET /admin/members/{authentik_sub}/permission-groups`
|
||||
- `PUT /admin/members/{authentik_sub}/permission-groups`
|
||||
- 群組:
|
||||
- `GET|POST|PATCH /admin/permission-groups`
|
||||
- `GET /admin/permission-groups/{group_key}/permissions`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/grant`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/revoke`
|
||||
- 直接授權:
|
||||
- `POST /admin/permissions/grant`
|
||||
- `POST /admin/permissions/revoke`
|
||||
- `GET /admin/permissions/direct`
|
||||
- `DELETE /admin/permissions/direct/{permission_id}`
|
||||
|
||||
## 主要 API
|
||||
- `GET /me`
|
||||
- `GET /me/permissions/snapshot`
|
||||
- `POST /admin/permissions/grant|revoke`
|
||||
- `GET|POST /admin/systems`
|
||||
- `GET|POST /admin/modules`
|
||||
- `GET|POST /admin/companies`
|
||||
- `GET|POST /admin/sites`
|
||||
- `GET /admin/members`
|
||||
- `GET|POST /admin/permission-groups`
|
||||
- `POST|DELETE /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/grant|revoke`
|
||||
- `GET /internal/systems|modules|companies|sites|members`
|
||||
## 驗證與查詢 API
|
||||
- 使用者端:
|
||||
- `GET /me`
|
||||
- `GET /me/permissions/snapshot`
|
||||
- OIDC:
|
||||
- `GET /auth/oidc/url`
|
||||
- `POST /auth/oidc/exchange`
|
||||
- Internal(跨系統查詢):
|
||||
- `GET /internal/systems|modules|companies|sites|members`
|
||||
- `GET /internal/permissions/{authentik_sub}/snapshot`
|
||||
|
||||
## DB Migration
|
||||
- 初始化:`backend/scripts/init_schema.sql`
|
||||
- 舊庫補齊:`backend/scripts/migrate_align_company_site_member_system.sql`
|
||||
## DB 與初始化
|
||||
- 統一 schema:`backend/scripts/init_schema.sql`
|
||||
- schema 快照:`docs/DB_SCHEMA_SNAPSHOT.md`
|
||||
|
||||
@@ -61,26 +61,48 @@ Headers:
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
- `GET/POST /admin/systems`
|
||||
- `GET/POST /admin/modules`
|
||||
- `GET/POST /admin/companies`
|
||||
- `GET/POST /admin/sites`
|
||||
- `GET/POST/PATCH /admin/systems`
|
||||
- `GET/POST/PATCH /admin/modules`
|
||||
- `GET/POST/PATCH /admin/companies`
|
||||
- `GET/POST/PATCH /admin/sites`
|
||||
- `GET /admin/members`
|
||||
- `POST /admin/members/upsert`
|
||||
- `PATCH /admin/members/{authentik_sub}`
|
||||
|
||||
## 4. 權限群組(一組權限綁多個 user)
|
||||
## 4. 會員與群組關聯(由會員頁管理)
|
||||
Headers:
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
- `GET/POST /admin/permission-groups`
|
||||
- `POST /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- `DELETE /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- `GET /admin/members/{authentik_sub}/permission-groups`
|
||||
- `PUT /admin/members/{authentik_sub}/permission-groups`
|
||||
```json
|
||||
{
|
||||
"group_keys": ["site-ops", "mkt-admin"]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 權限群組(一組權限可綁多個 user)
|
||||
Headers:
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
- `GET/POST/PATCH /admin/permission-groups`
|
||||
- `GET /admin/permission-groups/{group_key}/permissions`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/grant`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/revoke`
|
||||
|
||||
群組授權 payload 與 user 授權 payload 相同(用 `system/module/scope/action`)。
|
||||
|
||||
## 5. Internal 查詢 API(其他系統)
|
||||
## 6. 直接授權列表(權限管理頁)
|
||||
Headers:
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
- `GET /admin/permissions/direct?keyword=&scope_type=&limit=&offset=`
|
||||
- `DELETE /admin/permissions/direct/{permission_id}`
|
||||
|
||||
## 7. Internal 查詢 API(其他系統)
|
||||
Headers:
|
||||
- `X-Internal-Secret`
|
||||
|
||||
@@ -91,10 +113,11 @@ Headers:
|
||||
- `GET /internal/members`
|
||||
- `GET /internal/permissions/{authentik_sub}/snapshot`
|
||||
|
||||
## 6. 常見錯誤
|
||||
## 8. 常見錯誤
|
||||
- `401 invalid_client`
|
||||
- `401 invalid_api_key`
|
||||
- `401 invalid_internal_secret`
|
||||
- `404 system_not_found`
|
||||
- `404 company_not_found`
|
||||
- `404 site_not_found`
|
||||
- `400 invalid_permission_id`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Frontend 交辦清單(Schema v2)✅ 已完成
|
||||
# Frontend 交辦清單(Schema v2)
|
||||
|
||||
## 目標
|
||||
前端實現對應後端新模型:
|
||||
@@ -60,23 +60,27 @@
|
||||
- [x] 表格顯示三個欄位
|
||||
- [x] Dialog 表單新增站台
|
||||
|
||||
### 7) 會員列表 `/admin/members` ✅
|
||||
### 7) 會員管理 `/admin/members` ✅
|
||||
- [x] 列表:`GET /admin/members`
|
||||
- [x] 表格顯示 authentik_sub、email、display_name
|
||||
- [x] 可重新整理
|
||||
- [x] 新增會員:`POST /admin/members/upsert`
|
||||
- [x] 編輯會員:`PATCH /admin/members/{authentik_sub}`
|
||||
- [x] 會員頁可直接設定「權限群組」(multi-select):
|
||||
- [x] `GET /admin/members/{authentik_sub}/permission-groups`
|
||||
- [x] `PUT /admin/members/{authentik_sub}/permission-groups`
|
||||
|
||||
### 8) 權限群組 `/admin/permission-groups` ✅
|
||||
- [x] 群組管理 Tab:
|
||||
- [x] 列表:`GET /admin/permission-groups`
|
||||
- [x] 新增:`POST /admin/permission-groups`
|
||||
- [x] Dialog 表單新增群組
|
||||
- [x] 綁定會員 Tab:
|
||||
- [x] `POST /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- [x] UI 支援群組選擇 + authentik_sub 輸入 + 加入按鈕
|
||||
- [x] 群組授權 Tab:
|
||||
- [x] `POST /admin/permission-groups/{group_key}/permissions/grant`
|
||||
- [x] `POST /admin/permission-groups/{group_key}/permissions/revoke`
|
||||
- [x] UI 支援選擇群組、輸入權限資訊、grant/revoke 按鈕
|
||||
- [x] 群組權限列表:
|
||||
- [x] `GET /admin/permission-groups/{group_key}/permissions`
|
||||
- [x] 可查看群組目前有哪些系統/模組/操作權限
|
||||
|
||||
## 共用資料管理 ✅
|
||||
- [x] admin.js store 實現:
|
||||
@@ -88,11 +92,19 @@
|
||||
- [x] `X-Client-Key`
|
||||
- [x] `X-API-Key`
|
||||
- [x] axios adminHttp client 自動注入 headers
|
||||
- [x] 管理頁不需手動輸入金鑰(改由環境變數與攔截器帶入)
|
||||
|
||||
## 權限管理頁強化 ✅
|
||||
- [x] 直接授權列表:
|
||||
- [x] `GET /admin/permissions/direct`
|
||||
- [x] 支援關鍵字與 scope 篩選
|
||||
- [x] 列表逐筆撤銷:
|
||||
- [x] `DELETE /admin/permissions/direct/{permission_id}`
|
||||
|
||||
## 驗收條件 ✅
|
||||
- [x] 可以新增 system/module/company/site
|
||||
- [x] 可以做 user 直接 grant/revoke(新 payload)
|
||||
- [x] 可以建立 permission-group、加會員、做群組 grant/revoke
|
||||
- [x] 可以建立 permission-group,並在會員頁指派群組,做群組 grant/revoke
|
||||
- [x] `/me/permissions/snapshot` 能看到所有權限欄位(scope_type/scope_id/system/module/action)
|
||||
|
||||
## 完成日期
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
- [x] `/admin/modules`(模組 CRUD)
|
||||
- [x] `/admin/companies`(公司 CRUD)
|
||||
- [x] `/admin/sites`(站台 CRUD)
|
||||
- [x] `/admin/members`(會員列表)
|
||||
- [x] `/admin/permission-groups`(群組 CRUD + 綁會員 + 群組授權)
|
||||
- [x] `/admin/members`(會員 CRUD + 指派群組)
|
||||
- [x] `/admin/permission-groups`(群組 CRUD + 群組授權 + 群組權限列表)
|
||||
- [x] 導覽列加入管理員群組下拉菜單
|
||||
|
||||
## 進行中(下一階段)
|
||||
@@ -55,6 +55,6 @@
|
||||
- [x] 登入後可穩定讀取 `/me` 與快照 ✅
|
||||
- [x] 可新增 system/module/company/site ✅
|
||||
- [x] 可做用戶直接 grant/revoke(新 payload) ✅
|
||||
- [x] 可建立 permission-group、加會員、群組 grant/revoke ✅
|
||||
- [x] 可建立 permission-group,並從會員頁指派群組,做群組 grant/revoke ✅
|
||||
- [x] `/me/permissions/snapshot` 表格可顯示 system + module + action ✅
|
||||
- [x] 與後端契約文件一致 ✅
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
# member docs index
|
||||
|
||||
## 先看這三份
|
||||
1. `docs/FRONTEND_ARCHITECTURE.md`
|
||||
2. `docs/FRONTEND_API_CONTRACT.md`
|
||||
3. `docs/FRONTEND_IMPLEMENTATION_CHECKLIST.md`
|
||||
## 0. 先看這兩份(入口)
|
||||
1. `docs/ARCHITECTURE_AND_CONFIG.md`
|
||||
2. `docs/BACKEND_ARCHITECTURE.md`
|
||||
|
||||
## 系統架構與後端
|
||||
- `docs/ARCHITECTURE_AND_CONFIG.md`
|
||||
- `docs/BACKEND_ARCHITECTURE.md`
|
||||
## 1. 架構核心(你現在的實際模型)
|
||||
- 業務層級:`公司 companies -> 品牌站台 sites -> 會員 users`
|
||||
- 功能層級:`系統 systems -> 模組 modules`
|
||||
- 授權層級:`scope(company/site) + system + module(可空) + action`
|
||||
- 權限來源:`直接授權 + 群組授權`
|
||||
|
||||
## 2. 前端交辦(直接丟給另一隻 AI)
|
||||
1. `docs/FRONTEND_API_CONTRACT.md`
|
||||
2. `docs/FRONTEND_HANDOFF_SCHEMA_V2.md`
|
||||
3. `docs/FRONTEND_ARCHITECTURE.md`
|
||||
|
||||
## 3. 後端與環境
|
||||
- `docs/BACKEND_BOOTSTRAP.md`
|
||||
|
||||
## 任務管理
|
||||
- `docs/TASKPLAN_FRONTEND.md`
|
||||
- `docs/TASKPLAN_BACKEND.md`
|
||||
- `docs/ORG_MEMBER_MANAGEMENT_PLAN.md`(公司組織/會員管理規劃)
|
||||
- `docs/FRONTEND_HANDOFF_SCHEMA_V2.md`(前端交辦清單,直接給另一隻 AI)
|
||||
- `backend/.env.development`(本機開發)
|
||||
|
||||
## SQL 與配置
|
||||
- `backend/scripts/init_schema.sql`
|
||||
- `docs/DB_SCHEMA_SNAPSHOT.md`
|
||||
## 4. DB(單一來源)
|
||||
- `backend/scripts/init_schema.sql`(完整 schema)
|
||||
- `docs/DB_SCHEMA_SNAPSHOT.md`(目前資料庫結構快照)
|
||||
|
||||
## 給前端 AI 的一句話交接
|
||||
請先完成 `/me`、`/me/permissions/snapshot`、`/admin/permissions/grant|revoke` 三組 API 對接,並依 `FRONTEND_IMPLEMENTATION_CHECKLIST.md` 逐項完成。
|
||||
## 5. 管理流程(建議操作順序)
|
||||
1. 建立 `systems`、`modules`
|
||||
2. 建立 `companies`、`sites`
|
||||
3. 建立/同步 `members`(可同步 Authentik)
|
||||
4. 建立 `permission-groups`
|
||||
5. 在會員頁指定會員所屬群組
|
||||
6. 在權限頁做直接授權,或在群組頁做群組授權
|
||||
|
||||
## 6. 前端頁面責任切分
|
||||
- 會員頁:會員基本資料 + 群組指派
|
||||
- 群組頁:群組 CRUD + 群組權限列表 + 群組授權/撤銷
|
||||
- 權限管理頁:直接授權/撤銷 + 直接授權列表(可逐筆撤銷)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<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="權限群組">
|
||||
<el-select v-model="createForm.group_keys" multiple filterable clearable style="width: 100%" placeholder="可選多個群組">
|
||||
<el-option v-for="g in groups" :key="g.group_key" :label="`${g.name} (${g.group_key})`" :value="g.group_key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="啟用"><el-switch v-model="createForm.is_active" /></el-form-item>
|
||||
<el-form-item label="同步 Authentik"><el-switch v-model="createForm.sync_to_authentik" /></el-form-item>
|
||||
</el-form>
|
||||
@@ -44,6 +49,11 @@
|
||||
<el-form-item label="Authentik Sub"><el-input :model-value="editForm.authentik_sub" disabled /></el-form-item>
|
||||
<el-form-item label="Email"><el-input v-model="editForm.email" /></el-form-item>
|
||||
<el-form-item label="顯示名稱"><el-input v-model="editForm.display_name" /></el-form-item>
|
||||
<el-form-item label="權限群組">
|
||||
<el-select v-model="editForm.group_keys" multiple filterable clearable style="width: 100%" placeholder="可選多個群組">
|
||||
<el-option v-for="g in groups" :key="g.group_key" :label="`${g.name} (${g.group_key})`" :value="g.group_key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="啟用"><el-switch v-model="editForm.is_active" /></el-form-item>
|
||||
<el-form-item label="同步 Authentik"><el-switch v-model="editForm.sync_to_authentik" /></el-form-item>
|
||||
</el-form>
|
||||
@@ -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()
|
||||
|
||||
@@ -20,43 +20,13 @@
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEditGroup(row)">編輯</el-button>
|
||||
<el-button size="small" class="ml-2" @click="openPermissionsDialog(row)">權限</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Members Tab -->
|
||||
<el-tab-pane label="綁定會員" name="members">
|
||||
<div class="mt-4">
|
||||
<el-form :model="memberForm" label-width="120px" class="max-w-xl mb-4">
|
||||
<el-form-item label="Group Key">
|
||||
<el-select v-model="memberForm.groupKey" placeholder="選擇群組">
|
||||
<el-option v-for="g in groups" :key="g.group_key" :label="`${g.name} (${g.group_key})`" :value="g.group_key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Authentik Sub">
|
||||
<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-button type="primary" :loading="addingMember" @click="handleAddMember" :disabled="!memberForm.groupKey || !memberForm.authentikSub">
|
||||
加入群組
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-alert v-if="memberError" :title="memberError" type="error" show-icon :closable="false" class="mt-3" />
|
||||
<el-alert v-if="memberSuccess" :title="memberSuccess" type="success" show-icon :closable="false" class="mt-3" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Permissions Tab -->
|
||||
<el-tab-pane label="群組授權" name="permissions">
|
||||
<div class="mt-4">
|
||||
@@ -165,6 +135,23 @@
|
||||
<el-button type="primary" :loading="savingGroup" @click="handleEditGroup">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showPermissionsDialog" title="群組權限列表" width="900px">
|
||||
<div class="mb-3 text-sm text-gray-600">
|
||||
Group: <span class="font-medium">{{ selectedGroupKey }}</span>
|
||||
</div>
|
||||
<el-table :data="selectedGroupPermissions" border stripe v-loading="loadingGroupPermissions">
|
||||
<template #empty><el-empty description="此群組目前沒有權限" /></template>
|
||||
<el-table-column prop="scope_type" label="Scope" width="100" />
|
||||
<el-table-column prop="scope_id" label="Scope ID" min-width="140" />
|
||||
<el-table-column prop="system" label="系統" width="120" />
|
||||
<el-table-column prop="module" label="模組" width="180" />
|
||||
<el-table-column prop="action" label="操作" width="120" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showPermissionsDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,40 @@
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-card class="mt-6 shadow-sm">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="font-medium text-gray-700">已授權列表(直接授權)</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input v-model="listFilters.keyword" placeholder="搜尋 email/sub/module/action" clearable style="width: 280px" @keyup.enter="loadDirectPermissionList" />
|
||||
<el-select v-model="listFilters.scope_type" clearable placeholder="Scope" style="width: 140px">
|
||||
<el-option label="Company" value="company" />
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
<el-button :loading="listLoading" @click="loadDirectPermissionList">查詢</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="directPermissions" stripe border class="w-full" v-loading="listLoading">
|
||||
<template #empty><el-empty description="目前沒有直接授權資料" /></template>
|
||||
<el-table-column prop="display_name" label="名稱" min-width="140" />
|
||||
<el-table-column prop="email" label="Email" min-width="200" />
|
||||
<el-table-column prop="authentik_sub" label="Sub" min-width="200" />
|
||||
<el-table-column prop="scope_type" label="Scope" width="90" />
|
||||
<el-table-column prop="scope_id" label="Scope ID" min-width="120" />
|
||||
<el-table-column prop="system" label="系統" width="100" />
|
||||
<el-table-column prop="module" label="模組" width="130" />
|
||||
<el-table-column prop="action" label="操作" width="100" />
|
||||
<el-table-column prop="created_at" label="建立時間" min-width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small" @click="handleRevokeByRow(row)" :loading="revokeRowLoadingId === row.permission_id">撤銷</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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()])
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user