feat(admin): implement group-centric relations and system/module/company linkage views
This commit is contained in:
@@ -13,6 +13,10 @@ from app.repositories.users_repo import UsersRepository
|
|||||||
from app.schemas.catalog import (
|
from app.schemas.catalog import (
|
||||||
CompanyCreateRequest,
|
CompanyCreateRequest,
|
||||||
CompanyItem,
|
CompanyItem,
|
||||||
|
GroupBindingSnapshot,
|
||||||
|
GroupBindingUpdateRequest,
|
||||||
|
GroupRelationItem,
|
||||||
|
MemberRelationItem,
|
||||||
CompanyUpdateRequest,
|
CompanyUpdateRequest,
|
||||||
MemberItem,
|
MemberItem,
|
||||||
MemberPermissionGroupsResponse,
|
MemberPermissionGroupsResponse,
|
||||||
@@ -69,6 +73,14 @@ def _resolve_scope_ids(db: Session, scope_type: str, scope_id: str) -> tuple[str
|
|||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid_scope_type")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid_scope_type")
|
||||||
|
|
||||||
|
|
||||||
|
def _split_module_key(payload_module: str | None) -> str:
|
||||||
|
if not payload_module:
|
||||||
|
return "__system__"
|
||||||
|
if "." in payload_module:
|
||||||
|
return payload_module.split(".", 1)[1]
|
||||||
|
return payload_module
|
||||||
|
|
||||||
|
|
||||||
def _sync_member_to_authentik(
|
def _sync_member_to_authentik(
|
||||||
*,
|
*,
|
||||||
authentik_sub: str,
|
authentik_sub: str,
|
||||||
@@ -195,6 +207,96 @@ def update_module(
|
|||||||
return ModuleItem(id=row.id, system_key=system_key, module_key=row.module_key, name=row.name, status=row.status)
|
return ModuleItem(id=row.id, system_key=system_key, module_key=row.module_key, name=row.name, status=row.status)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/systems/{system_key}/groups")
|
||||||
|
def list_system_groups(
|
||||||
|
system_key: str,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
systems_repo = SystemsRepository(db)
|
||||||
|
groups_repo = PermissionGroupsRepository(db)
|
||||||
|
if not systems_repo.get_by_key(system_key):
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||||
|
groups = groups_repo.list_system_groups(system_key)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
GroupRelationItem(group_key=g.group_key, group_name=g.name, status=g.status).model_dump()
|
||||||
|
for g in groups
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/systems/{system_key}/members")
|
||||||
|
def list_system_members(
|
||||||
|
system_key: str,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
systems_repo = SystemsRepository(db)
|
||||||
|
groups_repo = PermissionGroupsRepository(db)
|
||||||
|
if not systems_repo.get_by_key(system_key):
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||||
|
members = groups_repo.list_system_members(system_key)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
MemberRelationItem(
|
||||||
|
authentik_sub=m.authentik_sub,
|
||||||
|
email=m.email,
|
||||||
|
display_name=m.display_name,
|
||||||
|
is_active=m.is_active,
|
||||||
|
).model_dump()
|
||||||
|
for m in members
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/modules/{module_key}/groups")
|
||||||
|
def list_module_groups(
|
||||||
|
module_key: str,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
modules_repo = ModulesRepository(db)
|
||||||
|
groups_repo = PermissionGroupsRepository(db)
|
||||||
|
module = modules_repo.get_by_key(module_key)
|
||||||
|
if not module:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found")
|
||||||
|
system_key, module_name = module.module_key.split(".", 1) if "." in module.module_key else ("", module.module_key)
|
||||||
|
groups = groups_repo.list_module_groups(system_key, module_name)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
GroupRelationItem(group_key=g.group_key, group_name=g.name, status=g.status).model_dump()
|
||||||
|
for g in groups
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/modules/{module_key}/members")
|
||||||
|
def list_module_members(
|
||||||
|
module_key: str,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
modules_repo = ModulesRepository(db)
|
||||||
|
groups_repo = PermissionGroupsRepository(db)
|
||||||
|
module = modules_repo.get_by_key(module_key)
|
||||||
|
if not module:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found")
|
||||||
|
system_key, module_name = module.module_key.split(".", 1) if "." in module.module_key else ("", module.module_key)
|
||||||
|
members = groups_repo.list_module_members(system_key, module_name)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
MemberRelationItem(
|
||||||
|
authentik_sub=m.authentik_sub,
|
||||||
|
email=m.email,
|
||||||
|
display_name=m.display_name,
|
||||||
|
is_active=m.is_active,
|
||||||
|
).model_dump()
|
||||||
|
for m in members
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/companies")
|
@router.get("/companies")
|
||||||
def list_companies(
|
def list_companies(
|
||||||
_: ApiClient = Depends(require_api_client),
|
_: ApiClient = Depends(require_api_client),
|
||||||
@@ -236,6 +338,32 @@ def update_company(
|
|||||||
return CompanyItem(id=row.id, company_key=row.company_key, name=row.name, status=row.status)
|
return CompanyItem(id=row.id, company_key=row.company_key, name=row.name, status=row.status)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/companies/{company_key}/sites")
|
||||||
|
def list_company_sites(
|
||||||
|
company_key: str,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
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")
|
||||||
|
items, _ = sites_repo.list(company_id=company.id, limit=1000, offset=0)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
SiteItem(
|
||||||
|
id=i.id,
|
||||||
|
site_key=i.site_key,
|
||||||
|
company_key=company.company_key,
|
||||||
|
name=i.name,
|
||||||
|
status=i.status,
|
||||||
|
).model_dump()
|
||||||
|
for i in items
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/sites")
|
@router.get("/sites")
|
||||||
def list_sites(
|
def list_sites(
|
||||||
_: ApiClient = Depends(require_api_client),
|
_: ApiClient = Depends(require_api_client),
|
||||||
@@ -481,16 +609,78 @@ def list_permission_group_permissions(
|
|||||||
PermissionGroupPermissionItem(
|
PermissionGroupPermissionItem(
|
||||||
id=r.id,
|
id=r.id,
|
||||||
system=r.system,
|
system=r.system,
|
||||||
module=r.module,
|
module=("" if r.module == "__system__" else (r.module if "." in r.module else f"{r.system}.{r.module}")),
|
||||||
action=r.action,
|
action=r.action,
|
||||||
scope_type=r.scope_type,
|
scope_type=r.scope_type,
|
||||||
scope_id=r.scope_id,
|
scope_id=r.scope_id,
|
||||||
).model_dump()
|
).model_dump()
|
||||||
for r in rows
|
for r in rows
|
||||||
|
if r.action in {"view", "edit"} and r.scope_type == "site"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/permission-groups/{group_key}/bindings", response_model=GroupBindingSnapshot)
|
||||||
|
def get_permission_group_bindings(
|
||||||
|
group_key: str,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> GroupBindingSnapshot:
|
||||||
|
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")
|
||||||
|
snapshot = repo.get_group_binding_snapshot(group.id, group_key)
|
||||||
|
return GroupBindingSnapshot(**snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/permission-groups/{group_key}/bindings", response_model=GroupBindingSnapshot)
|
||||||
|
def replace_permission_group_bindings(
|
||||||
|
group_key: str,
|
||||||
|
payload: GroupBindingUpdateRequest,
|
||||||
|
_: ApiClient = Depends(require_api_client),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> GroupBindingSnapshot:
|
||||||
|
repo = PermissionGroupsRepository(db)
|
||||||
|
sites_repo = SitesRepository(db)
|
||||||
|
systems_repo = SystemsRepository(db)
|
||||||
|
modules_repo = ModulesRepository(db)
|
||||||
|
|
||||||
|
group = repo.get_by_key(group_key)
|
||||||
|
if not group:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||||
|
|
||||||
|
site_keys = list(dict.fromkeys(payload.site_keys))
|
||||||
|
system_keys = list(dict.fromkeys(payload.system_keys))
|
||||||
|
module_keys = list(dict.fromkeys(payload.module_keys))
|
||||||
|
|
||||||
|
valid_sites = {s.site_key for s in sites_repo.list(limit=10000, offset=0)[0]}
|
||||||
|
missing_sites = [k for k in site_keys if k not in valid_sites]
|
||||||
|
if missing_sites:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"site_not_found:{','.join(missing_sites)}")
|
||||||
|
|
||||||
|
valid_systems = {s.system_key for s in systems_repo.list(limit=10000, offset=0)[0]}
|
||||||
|
missing_systems = [k for k in system_keys if k not in valid_systems]
|
||||||
|
if missing_systems:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"system_not_found:{','.join(missing_systems)}")
|
||||||
|
|
||||||
|
valid_modules = {m.module_key for m in modules_repo.list(limit=10000, offset=0)[0]}
|
||||||
|
missing_modules = [k for k in module_keys if k not in valid_modules]
|
||||||
|
if missing_modules:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"module_not_found:{','.join(missing_modules)}")
|
||||||
|
|
||||||
|
repo.replace_group_bindings(
|
||||||
|
group_id=group.id,
|
||||||
|
site_keys=site_keys,
|
||||||
|
system_keys=system_keys,
|
||||||
|
module_keys=module_keys,
|
||||||
|
member_subs=payload.member_subs,
|
||||||
|
actions=payload.actions,
|
||||||
|
)
|
||||||
|
snapshot = repo.get_group_binding_snapshot(group.id, group_key)
|
||||||
|
return GroupBindingSnapshot(**snapshot)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/permission-groups", response_model=PermissionGroupItem)
|
@router.post("/permission-groups", response_model=PermissionGroupItem)
|
||||||
def create_permission_group(
|
def create_permission_group(
|
||||||
payload: PermissionGroupCreateRequest,
|
payload: PermissionGroupCreateRequest,
|
||||||
@@ -562,11 +752,11 @@ def grant_group_permission(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||||
_resolve_module_id(db, payload.system, payload.module)
|
_resolve_module_id(db, payload.system, payload.module)
|
||||||
_resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
_resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||||
module_key = f"{payload.system}.{payload.module}" if payload.module else f"{payload.system}.__system__"
|
module_name = _split_module_key(payload.module)
|
||||||
row = groups_repo.grant_group_permission(
|
row = groups_repo.grant_group_permission(
|
||||||
group_id=group.id,
|
group_id=group.id,
|
||||||
system=payload.system,
|
system=payload.system,
|
||||||
module=module_key,
|
module=module_name,
|
||||||
action=payload.action,
|
action=payload.action,
|
||||||
scope_type=payload.scope_type,
|
scope_type=payload.scope_type,
|
||||||
scope_id=payload.scope_id,
|
scope_id=payload.scope_id,
|
||||||
@@ -587,11 +777,11 @@ def revoke_group_permission(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||||
_resolve_module_id(db, payload.system, payload.module)
|
_resolve_module_id(db, payload.system, payload.module)
|
||||||
_resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
_resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||||
module_key = f"{payload.system}.{payload.module}" if payload.module else f"{payload.system}.__system__"
|
module_name = _split_module_key(payload.module)
|
||||||
deleted = groups_repo.revoke_group_permission(
|
deleted = groups_repo.revoke_group_permission(
|
||||||
group_id=group.id,
|
group_id=group.id,
|
||||||
system=payload.system,
|
system=payload.system,
|
||||||
module=module_key,
|
module=module_name,
|
||||||
action=payload.action,
|
action=payload.action,
|
||||||
scope_type=payload.scope_type,
|
scope_type=payload.scope_type,
|
||||||
scope_id=payload.scope_id,
|
scope_id=payload.scope_id,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from sqlalchemy import delete, func, select
|
from sqlalchemy import delete, func, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models.permission_group import PermissionGroup
|
from app.models.permission_group import PermissionGroup
|
||||||
from app.models.permission_group_member import PermissionGroupMember
|
from app.models.permission_group_member import PermissionGroupMember
|
||||||
from app.models.permission_group_permission import PermissionGroupPermission
|
from app.models.permission_group_permission import PermissionGroupPermission
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
class PermissionGroupsRepository:
|
class PermissionGroupsRepository:
|
||||||
@@ -125,6 +128,128 @@ class PermissionGroupsRepository:
|
|||||||
)
|
)
|
||||||
return list(self.db.scalars(stmt).all())
|
return list(self.db.scalars(stmt).all())
|
||||||
|
|
||||||
|
def replace_group_bindings(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
group_id: str,
|
||||||
|
site_keys: list[str],
|
||||||
|
system_keys: list[str],
|
||||||
|
module_keys: list[str],
|
||||||
|
member_subs: list[str],
|
||||||
|
actions: list[str],
|
||||||
|
) -> None:
|
||||||
|
normalized_sites = list(dict.fromkeys([s for s in site_keys if s]))
|
||||||
|
normalized_actions = [a for a in list(dict.fromkeys(actions)) if a in {"view", "edit"}]
|
||||||
|
normalized_member_subs = list(dict.fromkeys([s for s in member_subs if s]))
|
||||||
|
|
||||||
|
modules_by_system: dict[str, list[str]] = defaultdict(list)
|
||||||
|
for full_module_key in list(dict.fromkeys([m for m in module_keys if m])):
|
||||||
|
if "." not in full_module_key:
|
||||||
|
continue
|
||||||
|
system_key, module_name = full_module_key.split(".", 1)
|
||||||
|
if module_name == "__system__":
|
||||||
|
continue
|
||||||
|
modules_by_system[system_key].append(module_name)
|
||||||
|
|
||||||
|
normalized_systems = set([s for s in system_keys if s])
|
||||||
|
normalized_systems.update(modules_by_system.keys())
|
||||||
|
|
||||||
|
self.db.execute(delete(PermissionGroupPermission).where(PermissionGroupPermission.group_id == group_id))
|
||||||
|
self.db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.group_id == group_id))
|
||||||
|
|
||||||
|
for sub in normalized_member_subs:
|
||||||
|
self.db.add(PermissionGroupMember(group_id=group_id, authentik_sub=sub))
|
||||||
|
|
||||||
|
for site_key in normalized_sites:
|
||||||
|
for action in normalized_actions:
|
||||||
|
for system_key in sorted(normalized_systems):
|
||||||
|
module_names = modules_by_system.get(system_key) or ["__system__"]
|
||||||
|
for module_name in module_names:
|
||||||
|
self.db.add(
|
||||||
|
PermissionGroupPermission(
|
||||||
|
group_id=group_id,
|
||||||
|
system=system_key,
|
||||||
|
module=module_name,
|
||||||
|
action=action,
|
||||||
|
scope_type="site",
|
||||||
|
scope_id=site_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def get_group_binding_snapshot(self, group_id: str, group_key: str) -> dict:
|
||||||
|
permissions = self.list_group_permissions(group_id)
|
||||||
|
site_keys = sorted({p.scope_id for p in permissions if p.scope_type == "site"})
|
||||||
|
system_keys = sorted({p.system for p in permissions})
|
||||||
|
actions = sorted({p.action for p in permissions if p.action in {"view", "edit"}})
|
||||||
|
module_keys = sorted(
|
||||||
|
{
|
||||||
|
p.module if "." in p.module else f"{p.system}.{p.module}"
|
||||||
|
for p in permissions
|
||||||
|
if p.module and p.module != "__system__"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
member_subs = sorted(self.list_group_member_subs(group_id))
|
||||||
|
return {
|
||||||
|
"group_key": group_key,
|
||||||
|
"site_keys": site_keys,
|
||||||
|
"system_keys": system_keys,
|
||||||
|
"module_keys": module_keys,
|
||||||
|
"member_subs": member_subs,
|
||||||
|
"actions": actions,
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_group_member_subs(self, group_id: str) -> list[str]:
|
||||||
|
stmt = (
|
||||||
|
select(PermissionGroupMember.authentik_sub)
|
||||||
|
.where(PermissionGroupMember.group_id == group_id)
|
||||||
|
.order_by(PermissionGroupMember.authentik_sub.asc())
|
||||||
|
)
|
||||||
|
return [row[0] for row in self.db.execute(stmt).all()]
|
||||||
|
|
||||||
|
def list_system_groups(self, system_key: str) -> list[PermissionGroup]:
|
||||||
|
stmt = (
|
||||||
|
select(PermissionGroup)
|
||||||
|
.join(PermissionGroupPermission, PermissionGroupPermission.group_id == PermissionGroup.id)
|
||||||
|
.where(PermissionGroupPermission.system == system_key)
|
||||||
|
.order_by(PermissionGroup.name.asc())
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
return list(self.db.scalars(stmt).all())
|
||||||
|
|
||||||
|
def list_system_members(self, system_key: str) -> list[User]:
|
||||||
|
stmt = (
|
||||||
|
select(User)
|
||||||
|
.join(PermissionGroupMember, PermissionGroupMember.authentik_sub == User.authentik_sub)
|
||||||
|
.join(PermissionGroupPermission, PermissionGroupPermission.group_id == PermissionGroupMember.group_id)
|
||||||
|
.where(PermissionGroupPermission.system == system_key)
|
||||||
|
.order_by(User.email.asc(), User.authentik_sub.asc())
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
return list(self.db.scalars(stmt).all())
|
||||||
|
|
||||||
|
def list_module_groups(self, system_key: str, module_name: str) -> list[PermissionGroup]:
|
||||||
|
stmt = (
|
||||||
|
select(PermissionGroup)
|
||||||
|
.join(PermissionGroupPermission, PermissionGroupPermission.group_id == PermissionGroup.id)
|
||||||
|
.where(PermissionGroupPermission.system == system_key, PermissionGroupPermission.module == module_name)
|
||||||
|
.order_by(PermissionGroup.name.asc())
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
return list(self.db.scalars(stmt).all())
|
||||||
|
|
||||||
|
def list_module_members(self, system_key: str, module_name: str) -> list[User]:
|
||||||
|
stmt = (
|
||||||
|
select(User)
|
||||||
|
.join(PermissionGroupMember, PermissionGroupMember.authentik_sub == User.authentik_sub)
|
||||||
|
.join(PermissionGroupPermission, PermissionGroupPermission.group_id == PermissionGroupMember.group_id)
|
||||||
|
.where(PermissionGroupPermission.system == system_key, PermissionGroupPermission.module == module_name)
|
||||||
|
.order_by(User.email.asc(), User.authentik_sub.asc())
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
return list(self.db.scalars(stmt).all())
|
||||||
|
|
||||||
def revoke_group_permission(
|
def revoke_group_permission(
|
||||||
self,
|
self,
|
||||||
group_id: str,
|
group_id: str,
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class PermissionsRepository:
|
|||||||
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
||||||
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
||||||
.where(UserScopePermission.user_id == user_id)
|
.where(UserScopePermission.user_id == user_id)
|
||||||
|
.where(UserScopePermission.action.in_(["view", "edit"]))
|
||||||
|
.where(UserScopePermission.scope_type == "site")
|
||||||
)
|
)
|
||||||
group_stmt = (
|
group_stmt = (
|
||||||
select(
|
select(
|
||||||
@@ -42,6 +44,8 @@ class PermissionsRepository:
|
|||||||
.select_from(PermissionGroupPermission)
|
.select_from(PermissionGroupPermission)
|
||||||
.join(PermissionGroupMember, PermissionGroupMember.group_id == PermissionGroupPermission.group_id)
|
.join(PermissionGroupMember, PermissionGroupMember.group_id == PermissionGroupPermission.group_id)
|
||||||
.where(PermissionGroupMember.authentik_sub == authentik_sub)
|
.where(PermissionGroupMember.authentik_sub == authentik_sub)
|
||||||
|
.where(PermissionGroupPermission.action.in_(["view", "edit"]))
|
||||||
|
.where(PermissionGroupPermission.scope_type == "site")
|
||||||
)
|
)
|
||||||
rows = self.db.execute(direct_stmt).all() + self.db.execute(group_stmt).all()
|
rows = self.db.execute(direct_stmt).all() + self.db.execute(group_stmt).all()
|
||||||
result: list[tuple[str, str, str | None, str, str]] = []
|
result: list[tuple[str, str, str | None, str, str]] = []
|
||||||
@@ -50,6 +54,10 @@ class PermissionsRepository:
|
|||||||
source = row[0]
|
source = row[0]
|
||||||
if source == "group":
|
if source == "group":
|
||||||
_, scope_type, scope_id, system_key, module_key, action = row
|
_, scope_type, scope_id, system_key, module_key, action = row
|
||||||
|
if module_key == "__system__":
|
||||||
|
module_key = f"{system_key}.__system__"
|
||||||
|
elif module_key and "." not in module_key:
|
||||||
|
module_key = f"{system_key}.{module_key}"
|
||||||
else:
|
else:
|
||||||
_, scope_type, company_key, site_key, module_key, action = row
|
_, scope_type, company_key, site_key, module_key, action = row
|
||||||
scope_id = company_key if scope_type == "company" else site_key
|
scope_id = company_key if scope_type == "company" else site_key
|
||||||
@@ -147,6 +155,8 @@ class PermissionsRepository:
|
|||||||
.join(Module, Module.id == UserScopePermission.module_id)
|
.join(Module, Module.id == UserScopePermission.module_id)
|
||||||
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
||||||
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
||||||
|
.where(UserScopePermission.action.in_(["view", "edit"]))
|
||||||
|
.where(UserScopePermission.scope_type == "site")
|
||||||
)
|
)
|
||||||
count_stmt = (
|
count_stmt = (
|
||||||
select(func.count())
|
select(func.count())
|
||||||
@@ -155,9 +165,11 @@ class PermissionsRepository:
|
|||||||
.join(Module, Module.id == UserScopePermission.module_id)
|
.join(Module, Module.id == UserScopePermission.module_id)
|
||||||
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
||||||
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
||||||
|
.where(UserScopePermission.action.in_(["view", "edit"]))
|
||||||
|
.where(UserScopePermission.scope_type == "site")
|
||||||
)
|
)
|
||||||
|
|
||||||
if scope_type in {"company", "site"}:
|
if scope_type == "site":
|
||||||
stmt = stmt.where(UserScopePermission.scope_type == scope_type)
|
stmt = stmt.where(UserScopePermission.scope_type == scope_type)
|
||||||
count_stmt = count_stmt.where(UserScopePermission.scope_type == scope_type)
|
count_stmt = count_stmt.where(UserScopePermission.scope_type == scope_type)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
class SystemCreateRequest(BaseModel):
|
class SystemCreateRequest(BaseModel):
|
||||||
@@ -134,11 +135,41 @@ class PermissionGroupPermissionItem(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
system: str
|
system: str
|
||||||
module: str
|
module: str
|
||||||
action: str
|
action: Literal["view", "edit"]
|
||||||
scope_type: str
|
scope_type: Literal["site"]
|
||||||
scope_id: str
|
scope_id: str
|
||||||
|
|
||||||
|
|
||||||
class MemberPermissionGroupsResponse(BaseModel):
|
class MemberPermissionGroupsResponse(BaseModel):
|
||||||
authentik_sub: str
|
authentik_sub: str
|
||||||
group_keys: list[str]
|
group_keys: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class GroupBindingUpdateRequest(BaseModel):
|
||||||
|
site_keys: list[str]
|
||||||
|
system_keys: list[str]
|
||||||
|
module_keys: list[str]
|
||||||
|
member_subs: list[str]
|
||||||
|
actions: list[Literal["view", "edit"]]
|
||||||
|
|
||||||
|
|
||||||
|
class GroupBindingSnapshot(BaseModel):
|
||||||
|
group_key: str
|
||||||
|
site_keys: list[str]
|
||||||
|
system_keys: list[str]
|
||||||
|
module_keys: list[str]
|
||||||
|
member_subs: list[str]
|
||||||
|
actions: list[Literal["view", "edit"]]
|
||||||
|
|
||||||
|
|
||||||
|
class GroupRelationItem(BaseModel):
|
||||||
|
group_key: str
|
||||||
|
group_name: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class MemberRelationItem(BaseModel):
|
||||||
|
authentik_sub: str
|
||||||
|
email: str | None = None
|
||||||
|
display_name: str | None = None
|
||||||
|
is_active: bool
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
ActionType = Literal["view", "edit"]
|
||||||
|
ScopeType = Literal["site"]
|
||||||
|
|
||||||
|
|
||||||
class PermissionGrantRequest(BaseModel):
|
class PermissionGrantRequest(BaseModel):
|
||||||
authentik_sub: str
|
authentik_sub: str
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
display_name: str | None = None
|
display_name: str | None = None
|
||||||
scope_type: str
|
scope_type: ScopeType
|
||||||
scope_id: str
|
scope_id: str
|
||||||
system: str
|
system: str
|
||||||
module: str | None = None
|
module: str | None = None
|
||||||
action: str
|
action: ActionType
|
||||||
|
|
||||||
|
|
||||||
class PermissionRevokeRequest(BaseModel):
|
class PermissionRevokeRequest(BaseModel):
|
||||||
authentik_sub: str
|
authentik_sub: str
|
||||||
scope_type: str
|
scope_type: ScopeType
|
||||||
scope_id: str
|
scope_id: str
|
||||||
system: str
|
system: str
|
||||||
module: str | None = None
|
module: str | None = None
|
||||||
action: str
|
action: ActionType
|
||||||
|
|
||||||
|
|
||||||
class PermissionItem(BaseModel):
|
class PermissionItem(BaseModel):
|
||||||
scope_type: str
|
scope_type: ScopeType
|
||||||
scope_id: str
|
scope_id: str
|
||||||
system: str | None = None
|
system: str | None = None
|
||||||
module: str
|
module: str
|
||||||
action: str
|
action: ActionType
|
||||||
|
|
||||||
|
|
||||||
class PermissionSnapshotResponse(BaseModel):
|
class PermissionSnapshotResponse(BaseModel):
|
||||||
@@ -41,11 +45,11 @@ class DirectPermissionRow(BaseModel):
|
|||||||
authentik_sub: str
|
authentik_sub: str
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
display_name: str | None = None
|
display_name: str | None = None
|
||||||
scope_type: str
|
scope_type: ScopeType
|
||||||
scope_id: str
|
scope_id: str
|
||||||
system: str | None = None
|
system: str | None = None
|
||||||
module: str | None = None
|
module: str | None = None
|
||||||
action: str
|
action: ActionType
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ const userTabs = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const adminTabs = [
|
const adminTabs = [
|
||||||
{ to: '/admin/permissions', label: '權限管理' },
|
|
||||||
{ to: '/admin/systems', label: '系統' },
|
{ to: '/admin/systems', label: '系統' },
|
||||||
{ to: '/admin/modules', label: '模組' },
|
{ to: '/admin/modules', label: '模組' },
|
||||||
{ to: '/admin/companies', label: '公司' },
|
{ to: '/admin/companies', label: '公司' },
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ import { adminHttp } from './http'
|
|||||||
export const getCompanies = () => adminHttp.get('/admin/companies')
|
export const getCompanies = () => adminHttp.get('/admin/companies')
|
||||||
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
|
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
|
||||||
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
|
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
|
||||||
|
export const getCompanySites = (companyKey) => adminHttp.get(`/admin/companies/${companyKey}/sites`)
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ import { adminHttp } from './http'
|
|||||||
export const getModules = () => adminHttp.get('/admin/modules')
|
export const getModules = () => adminHttp.get('/admin/modules')
|
||||||
export const createModule = (data) => adminHttp.post('/admin/modules', data)
|
export const createModule = (data) => adminHttp.post('/admin/modules', data)
|
||||||
export const updateModule = (moduleKey, data) => adminHttp.patch(`/admin/modules/${moduleKey}`, data)
|
export const updateModule = (moduleKey, data) => adminHttp.patch(`/admin/modules/${moduleKey}`, data)
|
||||||
|
export const getModuleGroups = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/groups`)
|
||||||
|
export const getModuleMembers = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/members`)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export const getPermissionGroups = () => adminHttp.get('/admin/permission-groups
|
|||||||
export const createPermissionGroup = (data) => adminHttp.post('/admin/permission-groups', data)
|
export const createPermissionGroup = (data) => adminHttp.post('/admin/permission-groups', data)
|
||||||
export const updatePermissionGroup = (groupKey, data) => adminHttp.patch(`/admin/permission-groups/${groupKey}`, 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 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) =>
|
||||||
|
adminHttp.put(`/admin/permission-groups/${groupKey}/bindings`, data)
|
||||||
|
|
||||||
export const addMemberToGroup = (groupKey, authentikSub) =>
|
export const addMemberToGroup = (groupKey, authentikSub) =>
|
||||||
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`)
|
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`)
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ import { adminHttp } from './http'
|
|||||||
export const getSystems = () => adminHttp.get('/admin/systems')
|
export const getSystems = () => adminHttp.get('/admin/systems')
|
||||||
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
|
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
|
||||||
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
|
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
|
||||||
|
export const getSystemGroups = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/groups`)
|
||||||
|
export const getSystemMembers = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/members`)
|
||||||
|
|||||||
@@ -13,9 +13,10 @@
|
|||||||
<el-table-column prop="company_key" label="Company Key" width="220" />
|
<el-table-column prop="company_key" label="Company Key" width="220" />
|
||||||
<el-table-column prop="name" label="名稱" min-width="200" />
|
<el-table-column prop="name" label="名稱" min-width="200" />
|
||||||
<el-table-column prop="status" label="狀態" width="120" />
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
<el-table-column label="操作" width="120">
|
<el-table-column label="操作" width="200">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||||
|
<el-button size="small" @click="openSites(row)">站台</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -47,6 +48,18 @@
|
|||||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="showSitesDialog" :title="`公司站台:${selectedCompanyKey}`" width="900px">
|
||||||
|
<el-table :data="companySites" border stripe v-loading="sitesLoading">
|
||||||
|
<template #empty><el-empty description="此公司目前沒有站台" /></template>
|
||||||
|
<el-table-column prop="site_key" label="Site Key" width="220" />
|
||||||
|
<el-table-column prop="name" label="名稱" min-width="220" />
|
||||||
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showSitesDialog = false">關閉</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,7 +67,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
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 { getCompanies, createCompany, updateCompany } from '@/api/companies'
|
import { getCompanies, createCompany, updateCompany, getCompanySites } from '@/api/companies'
|
||||||
|
|
||||||
const companies = ref([])
|
const companies = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -73,6 +86,11 @@ const rules = {
|
|||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showSitesDialog = ref(false)
|
||||||
|
const sitesLoading = ref(false)
|
||||||
|
const selectedCompanyKey = ref('')
|
||||||
|
const companySites = ref([])
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = false
|
error.value = false
|
||||||
@@ -133,5 +151,19 @@ async function handleEdit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openSites(row) {
|
||||||
|
selectedCompanyKey.value = row.company_key
|
||||||
|
showSitesDialog.value = true
|
||||||
|
sitesLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getCompanySites(row.company_key)
|
||||||
|
companySites.value = res.data?.items || []
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('載入公司站台失敗')
|
||||||
|
} finally {
|
||||||
|
sitesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,12 +19,14 @@
|
|||||||
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
|
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
|
||||||
<template #empty><el-empty description="目前無模組" /></template>
|
<template #empty><el-empty description="目前無模組" /></template>
|
||||||
<el-table-column prop="system_key" label="System" width="140" />
|
<el-table-column prop="system_key" label="System" width="140" />
|
||||||
<el-table-column prop="module_key" label="Module Key" width="180" />
|
<el-table-column prop="module_key" label="Module Key" width="220" />
|
||||||
<el-table-column prop="name" label="名稱" min-width="180" />
|
<el-table-column prop="name" label="名稱" min-width="180" />
|
||||||
<el-table-column prop="status" label="狀態" width="120" />
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
<el-table-column label="操作" width="120">
|
<el-table-column label="操作" width="260">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
<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>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -69,6 +71,33 @@
|
|||||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="showRelationDialog" :title="`模組關聯:${relationModuleKey}`" width="900px">
|
||||||
|
<el-tabs v-model="relationTab">
|
||||||
|
<el-tab-pane label="所屬群組" name="groups">
|
||||||
|
<el-table :data="relationGroups" border stripe v-loading="relationLoading">
|
||||||
|
<template #empty><el-empty description="尚無關聯群組" /></template>
|
||||||
|
<el-table-column prop="group_key" label="Group Key" width="220" />
|
||||||
|
<el-table-column prop="group_name" label="名稱" min-width="220" />
|
||||||
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="涉及會員" name="members">
|
||||||
|
<el-table :data="relationMembers" border stripe v-loading="relationLoading">
|
||||||
|
<template #empty><el-empty description="尚無關聯會員" /></template>
|
||||||
|
<el-table-column prop="authentik_sub" label="Authentik Sub" min-width="260" />
|
||||||
|
<el-table-column prop="email" label="Email" min-width="220" />
|
||||||
|
<el-table-column prop="display_name" label="顯示名稱" min-width="160" />
|
||||||
|
<el-table-column label="啟用" width="80">
|
||||||
|
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showRelationDialog = false">關閉</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,7 +105,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
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, getModuleGroups, getModuleMembers } from '@/api/modules'
|
||||||
import { getSystems } from '@/api/systems'
|
import { getSystems } from '@/api/systems'
|
||||||
|
|
||||||
const modules = ref([])
|
const modules = ref([])
|
||||||
@@ -98,6 +127,13 @@ const rules = {
|
|||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showRelationDialog = ref(false)
|
||||||
|
const relationLoading = ref(false)
|
||||||
|
const relationModuleKey = ref('')
|
||||||
|
const relationTab = ref('groups')
|
||||||
|
const relationGroups = ref([])
|
||||||
|
const relationMembers = ref([])
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = false
|
error.value = false
|
||||||
@@ -166,5 +202,24 @@ async function handleEdit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openRelations(row, tab) {
|
||||||
|
relationModuleKey.value = row.module_key
|
||||||
|
relationTab.value = tab
|
||||||
|
showRelationDialog.value = true
|
||||||
|
relationLoading.value = true
|
||||||
|
try {
|
||||||
|
const [groupsRes, membersRes] = await Promise.all([
|
||||||
|
getModuleGroups(row.module_key),
|
||||||
|
getModuleMembers(row.module_key)
|
||||||
|
])
|
||||||
|
relationGroups.value = groupsRes.data?.items || []
|
||||||
|
relationMembers.value = membersRes.data?.items || []
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('載入模組關聯資料失敗')
|
||||||
|
} finally {
|
||||||
|
relationLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,105 +1,96 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 mb-6">權限群組管理</h2>
|
<h2 class="text-xl font-bold text-gray-800 mb-6">群組與權限管理</h2>
|
||||||
|
|
||||||
<el-tabs v-model="activeTab" type="border-card" class="shadow-sm">
|
<el-card class="mb-6 shadow-sm">
|
||||||
<!-- Groups Tab -->
|
<template #header>
|
||||||
<el-tab-pane label="群組管理" name="groups">
|
<div class="flex items-center justify-between">
|
||||||
<div class="mt-4">
|
<span class="font-medium">群組列表</span>
|
||||||
<el-button type="primary" @click="showCreateGroup = true" :icon="Plus" class="mb-4">
|
<el-button type="primary" @click="showCreateGroup = true" :icon="Plus">新增群組</el-button>
|
||||||
新增群組
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<el-skeleton v-if="loadingGroups" :rows="4" animated />
|
|
||||||
|
|
||||||
<el-table v-else :data="groups" stripe border class="w-full">
|
|
||||||
<template #empty><el-empty description="目前無群組" /></template>
|
|
||||||
<el-table-column prop="group_key" label="Group Key" width="180" />
|
|
||||||
<el-table-column prop="name" label="群組名稱" min-width="200" />
|
|
||||||
<el-table-column prop="status" label="狀態" width="120" />
|
|
||||||
<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>
|
</div>
|
||||||
</el-tab-pane>
|
</template>
|
||||||
|
|
||||||
<!-- Permissions Tab -->
|
<el-skeleton v-if="loadingGroups" :rows="4" animated />
|
||||||
<el-tab-pane label="群組授權" name="permissions">
|
|
||||||
<div class="mt-4">
|
|
||||||
<el-form :model="groupPermForm" label-width="120px" class="max-w-xl mb-4">
|
|
||||||
<el-form-item label="Group Key">
|
|
||||||
<el-select v-model="groupPermForm.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="Scope Type">
|
|
||||||
<el-select v-model="groupPermForm.scope_type" placeholder="company or site">
|
|
||||||
<el-option label="Company" value="company" />
|
|
||||||
<el-option label="Site" value="site" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Scope ID">
|
|
||||||
<el-select v-model="groupPermForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
|
|
||||||
<el-option
|
|
||||||
v-for="s in scopeOptions"
|
|
||||||
:key="s.value"
|
|
||||||
:label="s.label"
|
|
||||||
:value="s.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="系統">
|
|
||||||
<el-select v-model="groupPermForm.system" 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 label="模組(選填)">
|
|
||||||
<el-select v-model="groupPermForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
|
|
||||||
<el-option
|
|
||||||
v-for="m in filteredModuleOptions"
|
|
||||||
:key="m.value"
|
|
||||||
:label="m.label"
|
|
||||||
:value="m.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="操作">
|
|
||||||
<el-select v-model="groupPermForm.action" filterable allow-create default-first-option style="width: 100%">
|
|
||||||
<el-option v-for="a in actionOptions" :key="a" :label="a" :value="a" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:loading="grantingGroupPerm"
|
|
||||||
@click="handleGroupGrant"
|
|
||||||
:disabled="!groupPermForm.groupKey || !groupPermForm.scope_type || !groupPermForm.scope_id || !groupPermForm.system || !groupPermForm.action"
|
|
||||||
>
|
|
||||||
Grant 授權
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
class="ml-2"
|
|
||||||
:loading="revokingGroupPerm"
|
|
||||||
@click="handleGroupRevoke"
|
|
||||||
:disabled="!groupPermForm.groupKey || !groupPermForm.scope_type || !groupPermForm.scope_id || !groupPermForm.system || !groupPermForm.action"
|
|
||||||
>
|
|
||||||
Revoke 撤銷
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<el-alert v-if="groupPermError" :title="groupPermError" type="error" show-icon :closable="false" class="mt-3" />
|
<el-table v-else :data="groups" stripe border class="w-full">
|
||||||
<el-alert v-if="groupPermSuccess" :title="groupPermSuccess" type="success" show-icon :closable="false" class="mt-3" />
|
<template #empty><el-empty description="目前無群組" /></template>
|
||||||
</div>
|
<el-table-column prop="group_key" label="Group Key" width="180" />
|
||||||
</el-tab-pane>
|
<el-table-column prop="name" label="群組名稱" min-width="220" />
|
||||||
</el-tabs>
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
|
<el-table-column label="操作" width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="openEditGroup(row)">編輯</el-button>
|
||||||
|
<el-button size="small" @click="openBindingDialog(row)">設定關聯</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="showBindingDialog" :title="`群組關聯設定:${bindingGroupKey}`" width="980px">
|
||||||
|
<el-form :model="bindingForm" label-width="130px">
|
||||||
|
<el-form-item label="站台(公司/站台)">
|
||||||
|
<el-select v-model="bindingForm.site_keys" multiple filterable clearable style="width: 100%" placeholder="選擇站台">
|
||||||
|
<el-option v-for="s in siteOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="系統(多選)">
|
||||||
|
<el-select v-model="bindingForm.system_keys" multiple filterable clearable style="width: 100%" placeholder="選擇系統">
|
||||||
|
<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 label="模組(多選)">
|
||||||
|
<el-select v-model="bindingForm.module_keys" multiple filterable clearable style="width: 100%" placeholder="選擇模組(可空,空值代表系統層)">
|
||||||
|
<el-option
|
||||||
|
v-for="m in filteredModuleOptions"
|
||||||
|
:key="m.module_key"
|
||||||
|
:label="`${m.name} (${m.module_key})`"
|
||||||
|
:value="m.module_key"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="會員(多選)">
|
||||||
|
<el-select v-model="bindingForm.member_subs" multiple filterable clearable style="width: 100%" placeholder="選擇會員">
|
||||||
|
<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 label="操作(多選)">
|
||||||
|
<el-select v-model="bindingForm.actions" multiple style="width: 100%" placeholder="選擇操作">
|
||||||
|
<el-option label="view" value="view" />
|
||||||
|
<el-option label="edit" value="edit" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
title="規則:scope 固定為 site;action 只允許 view/edit(可同時選)"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table :data="bindingPreview" border stripe v-loading="bindingLoading" max-height="260">
|
||||||
|
<template #empty><el-empty description="目前沒有授權規則" /></template>
|
||||||
|
<el-table-column prop="scope_display" label="公司/站台" min-width="220" />
|
||||||
|
<el-table-column prop="system" label="系統" width="120" />
|
||||||
|
<el-table-column prop="module" label="模組" min-width="180" />
|
||||||
|
<el-table-column prop="action" label="操作" width="100" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showBindingDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="savingBinding" @click="saveBindings">儲存關聯</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Create Group Dialog -->
|
|
||||||
<el-dialog v-model="showCreateGroup" title="新增群組" @close="resetCreateForm">
|
<el-dialog v-model="showCreateGroup" title="新增群組" @close="resetCreateForm">
|
||||||
<el-form :model="createForm" label-width="120px">
|
<el-form :model="createForm" label-width="120px">
|
||||||
<el-form-item label="Group Key">
|
<el-form-item label="Group Key">
|
||||||
@@ -135,28 +126,11 @@
|
|||||||
<el-button type="primary" :loading="savingGroup" @click="handleEditGroup">儲存</el-button>
|
<el-button type="primary" :loading="savingGroup" @click="handleEditGroup">儲存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
import { ref, reactive, onMounted, computed } 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 {
|
import {
|
||||||
@@ -164,46 +138,84 @@ import {
|
|||||||
createPermissionGroup,
|
createPermissionGroup,
|
||||||
updatePermissionGroup,
|
updatePermissionGroup,
|
||||||
getPermissionGroupPermissions,
|
getPermissionGroupPermissions,
|
||||||
groupGrant,
|
getPermissionGroupBindings,
|
||||||
groupRevoke
|
updatePermissionGroupBindings
|
||||||
} from '@/api/permission-groups'
|
} from '@/api/permission-groups'
|
||||||
import { getSystems } from '@/api/systems'
|
import { getSystems } from '@/api/systems'
|
||||||
import { getModules } from '@/api/modules'
|
import { getModules } from '@/api/modules'
|
||||||
import { getCompanies } from '@/api/companies'
|
|
||||||
import { getSites } from '@/api/sites'
|
import { getSites } from '@/api/sites'
|
||||||
|
import { getCompanies } from '@/api/companies'
|
||||||
|
import { getMembers } from '@/api/members'
|
||||||
|
|
||||||
const activeTab = ref('groups')
|
|
||||||
const systems = ref([])
|
|
||||||
const modules = ref([])
|
|
||||||
const companies = ref([])
|
|
||||||
const sites = ref([])
|
|
||||||
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
|
||||||
|
|
||||||
const filteredModuleOptions = computed(() => {
|
|
||||||
if (!groupPermForm.system) return []
|
|
||||||
return modules.value
|
|
||||||
.filter(m => m.system_key === groupPermForm.system && !m.module_key.endsWith('.__system__'))
|
|
||||||
.map(m => ({
|
|
||||||
value: m.module_key.startsWith(`${groupPermForm.system}.`)
|
|
||||||
? m.module_key.slice(groupPermForm.system.length + 1)
|
|
||||||
: m.module_key,
|
|
||||||
label: `${m.name} (${m.module_key})`
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const scopeOptions = computed(() => {
|
|
||||||
if (groupPermForm.scope_type === 'company') {
|
|
||||||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
|
||||||
}
|
|
||||||
if (groupPermForm.scope_type === 'site') {
|
|
||||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
// Groups
|
|
||||||
const groups = ref([])
|
const groups = ref([])
|
||||||
const loadingGroups = ref(false)
|
const loadingGroups = ref(false)
|
||||||
|
const systems = ref([])
|
||||||
|
const modules = ref([])
|
||||||
|
const sites = ref([])
|
||||||
|
const companies = ref([])
|
||||||
|
const members = ref([])
|
||||||
|
|
||||||
|
const showCreateGroup = ref(false)
|
||||||
|
const creatingGroup = ref(false)
|
||||||
|
const createForm = reactive({ group_key: '', name: '' })
|
||||||
|
|
||||||
|
const showEditGroup = ref(false)
|
||||||
|
const savingGroup = ref(false)
|
||||||
|
const editGroupForm = reactive({ group_key: '', name: '', status: 'active' })
|
||||||
|
|
||||||
|
const showBindingDialog = ref(false)
|
||||||
|
const bindingGroupKey = ref('')
|
||||||
|
const bindingLoading = ref(false)
|
||||||
|
const savingBinding = ref(false)
|
||||||
|
const bindingPreview = ref([])
|
||||||
|
const bindingForm = reactive({
|
||||||
|
site_keys: [],
|
||||||
|
system_keys: [],
|
||||||
|
module_keys: [],
|
||||||
|
member_subs: [],
|
||||||
|
actions: ['view']
|
||||||
|
})
|
||||||
|
|
||||||
|
const companyLookup = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
for (const c of companies.value) map[c.company_key] = c.name
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const siteOptions = computed(() => {
|
||||||
|
return sites.value.map(s => {
|
||||||
|
const companyName = companyLookup.value[s.company_key] || s.company_key
|
||||||
|
return {
|
||||||
|
value: s.site_key,
|
||||||
|
label: `${companyName}/${s.name} (${s.site_key})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredModuleOptions = computed(() => {
|
||||||
|
if (bindingForm.system_keys.length === 0) return modules.value
|
||||||
|
return modules.value.filter(m => bindingForm.system_keys.includes(m.system_key) && !m.module_key.endsWith('.__system__'))
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetCreateForm() {
|
||||||
|
createForm.group_key = ''
|
||||||
|
createForm.name = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEditGroupForm() {
|
||||||
|
editGroupForm.group_key = ''
|
||||||
|
editGroupForm.name = ''
|
||||||
|
editGroupForm.status = 'active'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBindingForm() {
|
||||||
|
bindingForm.site_keys = []
|
||||||
|
bindingForm.system_keys = []
|
||||||
|
bindingForm.module_keys = []
|
||||||
|
bindingForm.member_subs = []
|
||||||
|
bindingForm.actions = ['view']
|
||||||
|
bindingPreview.value = []
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGroups() {
|
async function loadGroups() {
|
||||||
loadingGroups.value = true
|
loadingGroups.value = true
|
||||||
@@ -211,10 +223,6 @@ async function loadGroups() {
|
|||||||
const res = await getPermissionGroups()
|
const res = await getPermissionGroups()
|
||||||
groups.value = res.data?.items || []
|
groups.value = res.data?.items || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response?.status === 422) {
|
|
||||||
ElMessage.error('缺少管理員 API 認證,請檢查前端 .env.development')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ElMessage.error('載入群組失敗')
|
ElMessage.error('載入群組失敗')
|
||||||
} finally {
|
} finally {
|
||||||
loadingGroups.value = false
|
loadingGroups.value = false
|
||||||
@@ -222,29 +230,18 @@ async function loadGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCatalogs() {
|
async function loadCatalogs() {
|
||||||
const [systemsRes, modulesRes, companiesRes, sitesRes] = await Promise.all([
|
const [systemsRes, modulesRes, sitesRes, companiesRes, membersRes] = await Promise.all([
|
||||||
getSystems(),
|
getSystems(),
|
||||||
getModules(),
|
getModules(),
|
||||||
|
getSites(),
|
||||||
getCompanies(),
|
getCompanies(),
|
||||||
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 || []
|
|
||||||
sites.value = sitesRes.data?.items || []
|
sites.value = sitesRes.data?.items || []
|
||||||
}
|
companies.value = companiesRes.data?.items || []
|
||||||
|
members.value = membersRes.data?.items || []
|
||||||
// Create Group
|
|
||||||
const showCreateGroup = ref(false)
|
|
||||||
const creatingGroup = ref(false)
|
|
||||||
const createForm = reactive({ group_key: '', name: '' })
|
|
||||||
const showEditGroup = ref(false)
|
|
||||||
const savingGroup = ref(false)
|
|
||||||
const editGroupForm = reactive({ group_key: '', name: '', status: 'active' })
|
|
||||||
|
|
||||||
function resetCreateForm() {
|
|
||||||
createForm.group_key = ''
|
|
||||||
createForm.name = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateGroup() {
|
async function handleCreateGroup() {
|
||||||
@@ -273,12 +270,6 @@ function openEditGroup(row) {
|
|||||||
showEditGroup.value = true
|
showEditGroup.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetEditGroupForm() {
|
|
||||||
editGroupForm.group_key = ''
|
|
||||||
editGroupForm.name = ''
|
|
||||||
editGroupForm.status = 'active'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEditGroup() {
|
async function handleEditGroup() {
|
||||||
savingGroup.value = true
|
savingGroup.value = true
|
||||||
try {
|
try {
|
||||||
@@ -296,77 +287,74 @@ async function handleEditGroup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPermissionsDialog = ref(false)
|
async function openBindingDialog(row) {
|
||||||
const loadingGroupPermissions = ref(false)
|
bindingGroupKey.value = row.group_key
|
||||||
const selectedGroupPermissions = ref([])
|
showBindingDialog.value = true
|
||||||
const selectedGroupKey = ref('')
|
bindingLoading.value = true
|
||||||
|
resetBindingForm()
|
||||||
async function openPermissionsDialog(row) {
|
|
||||||
selectedGroupKey.value = row.group_key
|
|
||||||
showPermissionsDialog.value = true
|
|
||||||
loadingGroupPermissions.value = true
|
|
||||||
try {
|
try {
|
||||||
const res = await getPermissionGroupPermissions(row.group_key)
|
const [bindingsRes, previewRes] = await Promise.all([
|
||||||
selectedGroupPermissions.value = res.data?.items || []
|
getPermissionGroupBindings(row.group_key),
|
||||||
|
getPermissionGroupPermissions(row.group_key)
|
||||||
|
])
|
||||||
|
const data = bindingsRes.data || {}
|
||||||
|
bindingForm.site_keys = data.site_keys || []
|
||||||
|
bindingForm.system_keys = data.system_keys || []
|
||||||
|
bindingForm.module_keys = data.module_keys || []
|
||||||
|
bindingForm.member_subs = data.member_subs || []
|
||||||
|
bindingForm.actions = (data.actions && data.actions.length > 0) ? data.actions : ['view']
|
||||||
|
bindingPreview.value = toPreview(previewRes.data?.items || [])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('載入群組權限失敗')
|
ElMessage.error('載入群組關聯失敗')
|
||||||
} finally {
|
} finally {
|
||||||
loadingGroupPermissions.value = false
|
bindingLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group Grant/Revoke
|
function toPreview(items) {
|
||||||
const groupPermForm = reactive({
|
const siteLabelMap = {}
|
||||||
groupKey: '',
|
for (const s of siteOptions.value) siteLabelMap[s.value] = s.label
|
||||||
scope_type: '',
|
return items.map(i => ({
|
||||||
scope_id: '',
|
...i,
|
||||||
system: '',
|
module: i.module || '(系統層)',
|
||||||
module: '',
|
scope_display: siteLabelMap[i.scope_id] || i.scope_id
|
||||||
action: ''
|
}))
|
||||||
})
|
|
||||||
const grantingGroupPerm = ref(false)
|
|
||||||
const revokingGroupPerm = ref(false)
|
|
||||||
const groupPermError = ref('')
|
|
||||||
const groupPermSuccess = ref('')
|
|
||||||
|
|
||||||
async function handleGroupGrant() {
|
|
||||||
groupPermError.value = ''
|
|
||||||
groupPermSuccess.value = ''
|
|
||||||
grantingGroupPerm.value = true
|
|
||||||
try {
|
|
||||||
const { groupKey, ...permData } = groupPermForm
|
|
||||||
await groupGrant(groupKey, permData)
|
|
||||||
groupPermSuccess.value = 'Grant 成功'
|
|
||||||
} catch (err) {
|
|
||||||
groupPermError.value = 'Grant 失敗'
|
|
||||||
} finally {
|
|
||||||
grantingGroupPerm.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGroupRevoke() {
|
async function saveBindings() {
|
||||||
groupPermError.value = ''
|
if (!bindingGroupKey.value) return
|
||||||
groupPermSuccess.value = ''
|
if (bindingForm.site_keys.length === 0) {
|
||||||
revokingGroupPerm.value = true
|
ElMessage.warning('至少需要選擇 1 個站台')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bindingForm.system_keys.length === 0 && bindingForm.module_keys.length === 0) {
|
||||||
|
ElMessage.warning('至少需要選擇 1 個系統或模組')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bindingForm.actions.length === 0) {
|
||||||
|
ElMessage.warning('至少需要選擇 1 個操作')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savingBinding.value = true
|
||||||
try {
|
try {
|
||||||
const { groupKey, ...permData } = groupPermForm
|
await updatePermissionGroupBindings(bindingGroupKey.value, {
|
||||||
await groupRevoke(groupKey, permData)
|
site_keys: bindingForm.site_keys,
|
||||||
groupPermSuccess.value = 'Revoke 成功'
|
system_keys: bindingForm.system_keys,
|
||||||
|
module_keys: bindingForm.module_keys,
|
||||||
|
member_subs: bindingForm.member_subs,
|
||||||
|
actions: bindingForm.actions
|
||||||
|
})
|
||||||
|
const previewRes = await getPermissionGroupPermissions(bindingGroupKey.value)
|
||||||
|
bindingPreview.value = toPreview(previewRes.data?.items || [])
|
||||||
|
ElMessage.success('群組關聯已更新')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
groupPermError.value = 'Revoke 失敗'
|
ElMessage.error(err.response?.data?.detail || '更新失敗')
|
||||||
} finally {
|
} finally {
|
||||||
revokingGroupPerm.value = false
|
savingBinding.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => groupPermForm.scope_type, () => {
|
|
||||||
groupPermForm.scope_id = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => groupPermForm.system, () => {
|
|
||||||
groupPermForm.module = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadGroups(), loadCatalogs()])
|
await Promise.all([loadGroups(), loadCatalogs()])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,9 +21,11 @@
|
|||||||
<el-table-column prop="system_key" label="System Key" width="200" />
|
<el-table-column prop="system_key" label="System Key" width="200" />
|
||||||
<el-table-column prop="name" label="名稱" min-width="180" />
|
<el-table-column prop="name" label="名稱" min-width="180" />
|
||||||
<el-table-column prop="status" label="狀態" width="120" />
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
<el-table-column label="操作" width="120">
|
<el-table-column label="操作" width="260">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
<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>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -63,6 +65,33 @@
|
|||||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="showRelationDialog" :title="`系統關聯:${relationSystemKey}`" width="900px">
|
||||||
|
<el-tabs v-model="relationTab">
|
||||||
|
<el-tab-pane label="所屬群組" name="groups">
|
||||||
|
<el-table :data="relationGroups" border stripe v-loading="relationLoading">
|
||||||
|
<template #empty><el-empty description="尚無關聯群組" /></template>
|
||||||
|
<el-table-column prop="group_key" label="Group Key" width="220" />
|
||||||
|
<el-table-column prop="group_name" label="名稱" min-width="220" />
|
||||||
|
<el-table-column prop="status" label="狀態" width="120" />
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="涉及會員" name="members">
|
||||||
|
<el-table :data="relationMembers" border stripe v-loading="relationLoading">
|
||||||
|
<template #empty><el-empty description="尚無關聯會員" /></template>
|
||||||
|
<el-table-column prop="authentik_sub" label="Authentik Sub" min-width="260" />
|
||||||
|
<el-table-column prop="email" label="Email" min-width="220" />
|
||||||
|
<el-table-column prop="display_name" label="顯示名稱" min-width="160" />
|
||||||
|
<el-table-column label="啟用" width="80">
|
||||||
|
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showRelationDialog = false">關閉</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -70,7 +99,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
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 { getSystems, createSystem, updateSystem } from '@/api/systems'
|
import { getSystems, createSystem, updateSystem, getSystemGroups, getSystemMembers } from '@/api/systems'
|
||||||
|
|
||||||
const systems = ref([])
|
const systems = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -89,6 +118,13 @@ const rules = {
|
|||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showRelationDialog = ref(false)
|
||||||
|
const relationLoading = ref(false)
|
||||||
|
const relationSystemKey = ref('')
|
||||||
|
const relationTab = ref('groups')
|
||||||
|
const relationGroups = ref([])
|
||||||
|
const relationMembers = ref([])
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = false
|
error.value = false
|
||||||
@@ -156,5 +192,24 @@ async function handleEdit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openRelations(row, tab) {
|
||||||
|
relationSystemKey.value = row.system_key
|
||||||
|
relationTab.value = tab
|
||||||
|
showRelationDialog.value = true
|
||||||
|
relationLoading.value = true
|
||||||
|
try {
|
||||||
|
const [groupsRes, membersRes] = await Promise.all([
|
||||||
|
getSystemGroups(row.system_key),
|
||||||
|
getSystemMembers(row.system_key)
|
||||||
|
])
|
||||||
|
relationGroups.value = groupsRes.data?.items || []
|
||||||
|
relationMembers.value = membersRes.data?.items || []
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('載入系統關聯資料失敗')
|
||||||
|
} finally {
|
||||||
|
relationLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Scope 類型" prop="scope_type">
|
<el-form-item label="Scope 類型" prop="scope_type">
|
||||||
<el-select v-model="grantForm.scope_type" placeholder="選擇 Scope 類型">
|
<el-select v-model="grantForm.scope_type" placeholder="選擇 Scope 類型">
|
||||||
<el-option label="Company" value="company" />
|
|
||||||
<el-option label="Site" value="site" />
|
<el-option label="Site" value="site" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -99,7 +98,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Scope 類型" prop="scope_type">
|
<el-form-item label="Scope 類型" prop="scope_type">
|
||||||
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
||||||
<el-option label="Company" value="company" />
|
|
||||||
<el-option label="Site" value="site" />
|
<el-option label="Site" value="site" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -162,7 +160,6 @@
|
|||||||
<div class="flex items-center gap-2">
|
<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-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-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-option label="Site" value="site" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button :loading="listLoading" @click="loadDirectPermissionList">查詢</el-button>
|
<el-button :loading="listLoading" @click="loadDirectPermissionList">查詢</el-button>
|
||||||
@@ -210,7 +207,7 @@ const modules = ref([])
|
|||||||
const companies = ref([])
|
const companies = ref([])
|
||||||
const sites = ref([])
|
const sites = ref([])
|
||||||
const members = ref([])
|
const members = ref([])
|
||||||
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
const actionOptions = ['view', 'edit']
|
||||||
const listFilters = reactive({ keyword: '', scope_type: '' })
|
const listFilters = reactive({ keyword: '', scope_type: '' })
|
||||||
const listLoading = ref(false)
|
const listLoading = ref(false)
|
||||||
const directPermissions = ref([])
|
const directPermissions = ref([])
|
||||||
@@ -246,9 +243,6 @@ const grantModuleOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const grantScopeOptions = computed(() => {
|
const grantScopeOptions = computed(() => {
|
||||||
if (grantForm.scope_type === 'company') {
|
|
||||||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
|
||||||
}
|
|
||||||
if (grantForm.scope_type === 'site') {
|
if (grantForm.scope_type === 'site') {
|
||||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||||
}
|
}
|
||||||
@@ -318,9 +312,6 @@ const revokeModuleOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const revokeScopeOptions = computed(() => {
|
const revokeScopeOptions = computed(() => {
|
||||||
if (revokeForm.scope_type === 'company') {
|
|
||||||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
|
||||||
}
|
|
||||||
if (revokeForm.scope_type === 'site') {
|
if (revokeForm.scope_type === 'site') {
|
||||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user