refactor: rebuild backend around role-site authorization model
This commit is contained in:
20
README.md
20
README.md
@@ -8,6 +8,7 @@ python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e .
|
||||
cp .env.example .env
|
||||
psql "$DATABASE_URL" -f scripts/init_schema.sql
|
||||
./scripts/start_dev.sh
|
||||
```
|
||||
|
||||
@@ -36,6 +37,25 @@ cp .env.example .env
|
||||
- `POST /auth/oidc/exchange`
|
||||
- `GET /me` (Bearer token required)
|
||||
- `GET /me/permissions/snapshot` (Bearer token required)
|
||||
|
||||
### Admin APIs (Bearer + admin group required)
|
||||
- `GET/POST/PATCH/DELETE /admin/companies`
|
||||
- `GET/POST/PATCH/DELETE /admin/sites`
|
||||
- `GET/POST/PATCH/DELETE /admin/systems`
|
||||
- `GET/POST/PATCH/DELETE /admin/roles`
|
||||
- `GET/POST/PATCH/DELETE /admin/members`
|
||||
- `PUT /admin/sites/{site_key}/roles`
|
||||
- `PUT /admin/members/{user_sub}/sites`
|
||||
- `GET /admin/members/{user_sub}/roles`
|
||||
- `GET/POST/PATCH/DELETE /admin/api-clients`
|
||||
|
||||
### Internal APIs (`X-Client-Key` + `X-API-Key`)
|
||||
- `GET /internal/companies`
|
||||
- `GET /internal/sites`
|
||||
- `GET /internal/systems`
|
||||
- `GET /internal/roles`
|
||||
- `GET /internal/members`
|
||||
- `POST /internal/users/upsert-by-sub`
|
||||
- `GET /internal/users/{user_sub}/roles`
|
||||
- `GET /internal/permissions/{user_sub}/snapshot`
|
||||
- `POST /internal/idp/users/ensure`
|
||||
|
||||
153
app/api/admin.py
153
app/api/admin.py
@@ -1,153 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.repositories.companies_repo import CompaniesRepository
|
||||
from app.repositories.modules_repo import ModulesRepository
|
||||
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 (
|
||||
DirectPermissionListResponse,
|
||||
DirectPermissionRow,
|
||||
PermissionGrantRequest,
|
||||
PermissionRevokeRequest,
|
||||
)
|
||||
from app.security.admin_guard import require_admin_principal
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin",
|
||||
tags=["admin"],
|
||||
dependencies=[Depends(require_admin_principal)],
|
||||
)
|
||||
|
||||
|
||||
def _resolve_module_id(db: Session, system_key: str, module_key: str | None) -> str:
|
||||
systems_repo = SystemsRepository(db)
|
||||
modules_repo = ModulesRepository(db)
|
||||
system = systems_repo.get_by_key(system_key)
|
||||
if not system:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||
|
||||
target_module_key = module_key if module_key else f"__system__{system_key}"
|
||||
module = modules_repo.get_by_key(target_module_key)
|
||||
if module and module.system_key != system_key:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="module_system_mismatch")
|
||||
if not module:
|
||||
module = modules_repo.create(
|
||||
module_key=target_module_key,
|
||||
system_key=system_key,
|
||||
name=target_module_key,
|
||||
status="active",
|
||||
)
|
||||
return module.id
|
||||
|
||||
|
||||
def _resolve_scope_ids(db: Session, scope_type: str, scope_id: str) -> tuple[str | None, str | None]:
|
||||
companies_repo = CompaniesRepository(db)
|
||||
sites_repo = SitesRepository(db)
|
||||
if scope_type == "company":
|
||||
company = companies_repo.get_by_key(scope_id)
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
|
||||
return company.id, None
|
||||
if scope_type == "site":
|
||||
site = sites_repo.get_by_key(scope_id)
|
||||
if not site:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="site_not_found")
|
||||
return None, site.id
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid_scope_type")
|
||||
|
||||
|
||||
@router.post("/permissions/grant")
|
||||
def grant_permission(
|
||||
payload: PermissionGrantRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, str]:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
|
||||
user = users_repo.upsert_by_sub(
|
||||
user_sub=payload.user_sub,
|
||||
username=None,
|
||||
email=payload.email,
|
||||
display_name=payload.display_name,
|
||||
is_active=True,
|
||||
)
|
||||
module_id = _resolve_module_id(db, payload.system, payload.module)
|
||||
company_id, site_id = _resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||
permission = perms_repo.create_if_not_exists(
|
||||
user_id=user.id,
|
||||
module_id=module_id,
|
||||
action=payload.action,
|
||||
scope_type=payload.scope_type,
|
||||
company_id=company_id,
|
||||
site_id=site_id,
|
||||
)
|
||||
|
||||
return {"permission_id": permission.id, "result": "granted"}
|
||||
|
||||
|
||||
@router.post("/permissions/revoke")
|
||||
def revoke_permission(
|
||||
payload: PermissionRevokeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, int | str]:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
|
||||
user = users_repo.get_by_sub(payload.user_sub)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
|
||||
|
||||
module_id = _resolve_module_id(db, payload.system, payload.module)
|
||||
company_id, site_id = _resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||
deleted = perms_repo.revoke(
|
||||
user_id=user.id,
|
||||
module_id=module_id,
|
||||
action=payload.action,
|
||||
scope_type=payload.scope_type,
|
||||
company_id=company_id,
|
||||
site_id=site_id,
|
||||
)
|
||||
return {"deleted": deleted, "result": "revoked"}
|
||||
|
||||
|
||||
@router.get("/permissions/direct", response_model=DirectPermissionListResponse)
|
||||
def list_direct_permissions(
|
||||
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,
|
||||
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"}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.db.session import get_db
|
||||
from app.repositories.permissions_repo import PermissionsRepository
|
||||
from app.schemas.internal import InternalUpsertUserBySubResponse
|
||||
from app.repositories.users_repo import UsersRepository
|
||||
from app.repositories.user_sites_repo import UserSitesRepository
|
||||
from app.schemas.idp_admin import KeycloakEnsureUserRequest, KeycloakEnsureUserResponse
|
||||
from app.schemas.permissions import PermissionSnapshotResponse
|
||||
from app.schemas.internal import InternalUpsertUserBySubResponse, InternalUserRoleItem, InternalUserRoleResponse
|
||||
from app.schemas.permissions import RoleSnapshotResponse
|
||||
from app.schemas.users import UserUpsertBySubRequest
|
||||
from app.security.api_client_auth import require_api_client
|
||||
from app.services.idp_admin_service import KeycloakAdminService
|
||||
@@ -28,32 +28,84 @@ def upsert_user_by_sub(
|
||||
email=payload.email,
|
||||
display_name=payload.display_name,
|
||||
is_active=payload.is_active,
|
||||
status=payload.status,
|
||||
)
|
||||
return InternalUpsertUserBySubResponse(
|
||||
id=user.id,
|
||||
user_sub=user.user_sub,
|
||||
idp_user_id=user.idp_user_id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
display_name=user.display_name,
|
||||
is_active=user.is_active,
|
||||
status=user.status,
|
||||
)
|
||||
return {
|
||||
"id": user.id,
|
||||
"user_sub": user.user_sub,
|
||||
"idp_user_id": user.idp_user_id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"display_name": user.display_name,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/permissions/{user_sub}/snapshot", response_model=PermissionSnapshotResponse)
|
||||
def get_permission_snapshot(
|
||||
user_sub: str,
|
||||
db: Session = Depends(get_db),
|
||||
) -> PermissionSnapshotResponse:
|
||||
def _build_user_role_rows(db: Session, user_sub: str) -> list[tuple[str, str, str, str, str, str, str, str, str]]:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
user_sites_repo = UserSitesRepository(db)
|
||||
|
||||
user = users_repo.get_by_sub(user_sub)
|
||||
if user is None:
|
||||
return PermissionSnapshotResponse(user_sub=user_sub, permissions=[])
|
||||
return []
|
||||
|
||||
permissions = perms_repo.list_by_user(user.id, user.user_sub)
|
||||
return PermissionService.build_snapshot(user_sub=user_sub, permissions=permissions)
|
||||
rows = user_sites_repo.get_user_role_rows(user.id)
|
||||
return [
|
||||
(
|
||||
site.site_key,
|
||||
site.display_name,
|
||||
company.company_key,
|
||||
company.display_name,
|
||||
system.system_key,
|
||||
system.name,
|
||||
role.role_key,
|
||||
role.name,
|
||||
role.idp_role_name,
|
||||
)
|
||||
for site, company, role, system in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/users/{user_sub}/roles", response_model=InternalUserRoleResponse)
|
||||
def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUserRoleResponse:
|
||||
rows = _build_user_role_rows(db, user_sub)
|
||||
return InternalUserRoleResponse(
|
||||
user_sub=user_sub,
|
||||
roles=[
|
||||
InternalUserRoleItem(
|
||||
site_key=site_key,
|
||||
site_display_name=site_display_name,
|
||||
company_key=company_key,
|
||||
company_display_name=company_display_name,
|
||||
system_key=system_key,
|
||||
system_name=system_name,
|
||||
role_key=role_key,
|
||||
role_name=role_name,
|
||||
idp_role_name=idp_role_name,
|
||||
)
|
||||
for (
|
||||
site_key,
|
||||
site_display_name,
|
||||
company_key,
|
||||
company_display_name,
|
||||
system_key,
|
||||
system_name,
|
||||
role_key,
|
||||
role_name,
|
||||
idp_role_name,
|
||||
) in rows
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/permissions/{user_sub}/snapshot", response_model=RoleSnapshotResponse)
|
||||
def get_permission_snapshot(
|
||||
user_sub: str,
|
||||
db: Session = Depends(get_db),
|
||||
) -> RoleSnapshotResponse:
|
||||
rows = _build_user_role_rows(db, user_sub)
|
||||
return PermissionService.build_role_snapshot(user_sub=user_sub, rows=rows)
|
||||
|
||||
|
||||
@router.post("/idp/users/ensure", response_model=KeycloakEnsureUserResponse)
|
||||
@@ -73,17 +125,17 @@ def ensure_idp_user(
|
||||
)
|
||||
|
||||
users_repo = UsersRepository(db)
|
||||
resolved_sub = payload.user_sub or ""
|
||||
if sync_result.user_sub:
|
||||
resolved_sub = sync_result.user_sub
|
||||
resolved_sub = payload.user_sub or sync_result.user_sub or ""
|
||||
if not resolved_sub:
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="idp_missing_sub")
|
||||
|
||||
users_repo.upsert_by_sub(
|
||||
user_sub=resolved_sub,
|
||||
username=payload.username,
|
||||
email=payload.email,
|
||||
display_name=payload.display_name,
|
||||
is_active=payload.is_active,
|
||||
status="active",
|
||||
idp_user_id=sync_result.user_id,
|
||||
)
|
||||
return KeycloakEnsureUserResponse(idp_user_id=sync_result.user_id, action=sync_result.action)
|
||||
|
||||
@@ -3,14 +3,15 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.repositories.companies_repo import CompaniesRepository
|
||||
from app.repositories.modules_repo import ModulesRepository
|
||||
from app.repositories.roles_repo import RolesRepository
|
||||
from app.repositories.sites_repo import SitesRepository
|
||||
from app.repositories.systems_repo import SystemsRepository
|
||||
from app.repositories.users_repo import UsersRepository
|
||||
from app.schemas.internal import (
|
||||
InternalCompanyListResponse,
|
||||
InternalMemberListResponse,
|
||||
InternalModuleListResponse,
|
||||
InternalRoleItem,
|
||||
InternalRoleListResponse,
|
||||
InternalSiteListResponse,
|
||||
InternalSystemListResponse,
|
||||
)
|
||||
@@ -27,24 +28,13 @@ def internal_list_systems(
|
||||
) -> InternalSystemListResponse:
|
||||
repo = SystemsRepository(db)
|
||||
items, total = repo.list(limit=limit, offset=offset)
|
||||
return {"items": [{"id": i.id, "system_key": i.system_key, "name": i.name, "status": i.status} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.get("/modules", response_model=InternalModuleListResponse)
|
||||
def internal_list_modules(
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = Query(default=500, ge=1, le=2000),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> InternalModuleListResponse:
|
||||
modules_repo = ModulesRepository(db)
|
||||
items, total = modules_repo.list(limit=limit, offset=offset)
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": i.id,
|
||||
"module_key": i.module_key,
|
||||
"system_key": i.system_key,
|
||||
"name": i.name,
|
||||
"idp_client_id": i.idp_client_id,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
@@ -55,6 +45,43 @@ def internal_list_modules(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/roles", response_model=InternalRoleListResponse)
|
||||
def internal_list_roles(
|
||||
db: Session = Depends(get_db),
|
||||
system_key: str | None = Query(default=None),
|
||||
limit: int = Query(default=500, ge=1, le=2000),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> InternalRoleListResponse:
|
||||
systems_repo = SystemsRepository(db)
|
||||
roles_repo = RolesRepository(db)
|
||||
|
||||
system_id = None
|
||||
systems, _ = systems_repo.list(limit=5000, offset=0)
|
||||
system_map = {s.id: s for s in systems}
|
||||
if system_key:
|
||||
system = systems_repo.get_by_key(system_key)
|
||||
if not system:
|
||||
return InternalRoleListResponse(items=[], total=0, limit=limit, offset=offset)
|
||||
system_id = system.id
|
||||
|
||||
items, total = roles_repo.list(system_id=system_id, limit=limit, offset=offset)
|
||||
rows = [
|
||||
InternalRoleItem(
|
||||
id=i.id,
|
||||
role_key=i.role_key,
|
||||
system_key=system_map[i.system_id].system_key,
|
||||
system_name=system_map[i.system_id].name,
|
||||
name=i.name,
|
||||
idp_role_name=i.idp_role_name,
|
||||
description=i.description,
|
||||
status=i.status,
|
||||
)
|
||||
for i in items
|
||||
if i.system_id in system_map
|
||||
]
|
||||
return InternalRoleListResponse(items=rows, total=total, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.get("/companies", response_model=InternalCompanyListResponse)
|
||||
def internal_list_companies(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -64,7 +91,21 @@ def internal_list_companies(
|
||||
) -> InternalCompanyListResponse:
|
||||
repo = CompaniesRepository(db)
|
||||
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
|
||||
return {"items": [{"id": i.id, "company_key": i.company_key, "name": i.name, "status": i.status} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": i.id,
|
||||
"company_key": i.company_key,
|
||||
"display_name": i.display_name,
|
||||
"legal_name": i.legal_name,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/sites", response_model=InternalSiteListResponse)
|
||||
@@ -81,10 +122,27 @@ def internal_list_sites(
|
||||
company = companies_repo.get_by_key(company_key)
|
||||
if company:
|
||||
company_id = company.id
|
||||
companies, _ = companies_repo.list(limit=2000, offset=0)
|
||||
mapping = {c.id: c.company_key for c in companies}
|
||||
companies, _ = companies_repo.list(limit=5000, offset=0)
|
||||
mapping = {c.id: c for c in companies}
|
||||
items, total = sites_repo.list(company_id=company_id, limit=limit, offset=offset)
|
||||
return {"items": [{"id": i.id, "site_key": i.site_key, "company_key": mapping.get(i.company_id), "name": i.name, "status": i.status} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": i.id,
|
||||
"site_key": i.site_key,
|
||||
"company_key": mapping[i.company_id].company_key,
|
||||
"company_display_name": mapping[i.company_id].display_name,
|
||||
"display_name": i.display_name,
|
||||
"domain": i.domain,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
if i.company_id in mapping
|
||||
],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/members", response_model=InternalMemberListResponse)
|
||||
@@ -105,6 +163,7 @@ def internal_list_members(
|
||||
"email": i.email,
|
||||
"display_name": i.display_name,
|
||||
"is_active": i.is_active,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
],
|
||||
|
||||
@@ -3,10 +3,10 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.repositories.permissions_repo import PermissionsRepository
|
||||
from app.repositories.users_repo import UsersRepository
|
||||
from app.repositories.user_sites_repo import UserSitesRepository
|
||||
from app.schemas.auth import KeycloakPrincipal, MeSummaryResponse
|
||||
from app.schemas.permissions import PermissionSnapshotResponse
|
||||
from app.schemas.permissions import RoleSnapshotResponse
|
||||
from app.security.idp_jwt import require_authenticated_principal
|
||||
from app.services.permission_service import PermissionService
|
||||
|
||||
@@ -26,10 +26,10 @@ def get_me(
|
||||
email=principal.email,
|
||||
display_name=principal.name or principal.preferred_username,
|
||||
is_active=True,
|
||||
status="active",
|
||||
)
|
||||
return MeSummaryResponse(sub=user.user_sub, email=user.email, display_name=user.display_name)
|
||||
except SQLAlchemyError:
|
||||
# DB schema compatibility fallback for local bring-up.
|
||||
return MeSummaryResponse(
|
||||
sub=principal.sub,
|
||||
email=principal.email,
|
||||
@@ -37,14 +37,14 @@ def get_me(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/permissions/snapshot", response_model=PermissionSnapshotResponse)
|
||||
@router.get("/permissions/snapshot", response_model=RoleSnapshotResponse)
|
||||
def get_my_permission_snapshot(
|
||||
principal: KeycloakPrincipal = Depends(require_authenticated_principal),
|
||||
db: Session = Depends(get_db),
|
||||
) -> PermissionSnapshotResponse:
|
||||
) -> RoleSnapshotResponse:
|
||||
try:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
user_sites_repo = UserSitesRepository(db)
|
||||
|
||||
user = users_repo.upsert_by_sub(
|
||||
user_sub=principal.sub,
|
||||
@@ -52,8 +52,23 @@ def get_my_permission_snapshot(
|
||||
email=principal.email,
|
||||
display_name=principal.name or principal.preferred_username,
|
||||
is_active=True,
|
||||
status="active",
|
||||
)
|
||||
permissions = perms_repo.list_by_user(user.id, user.user_sub)
|
||||
return PermissionService.build_snapshot(user_sub=principal.sub, permissions=permissions)
|
||||
rows = user_sites_repo.get_user_role_rows(user.id)
|
||||
serialized = [
|
||||
(
|
||||
site.site_key,
|
||||
site.display_name,
|
||||
company.company_key,
|
||||
company.display_name,
|
||||
system.system_key,
|
||||
system.name,
|
||||
role.role_key,
|
||||
role.name,
|
||||
role.idp_role_name,
|
||||
)
|
||||
for site, company, role, system in rows
|
||||
]
|
||||
return PermissionService.build_role_snapshot(user_sub=principal.sub, rows=serialized)
|
||||
except SQLAlchemyError:
|
||||
return PermissionSnapshotResponse(user_sub=principal.sub, permissions=[])
|
||||
return RoleSnapshotResponse(user_sub=principal.sub, roles=[])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.api.admin import router as admin_router
|
||||
from app.api.admin_catalog import router as admin_catalog_router
|
||||
from app.api.auth import router as auth_router
|
||||
from app.api.internal_catalog import router as internal_catalog_router
|
||||
@@ -28,7 +27,6 @@ def healthz() -> dict[str, str]:
|
||||
|
||||
app.include_router(internal_router)
|
||||
app.include_router(internal_catalog_router)
|
||||
app.include_router(admin_router)
|
||||
app.include_router(admin_catalog_router)
|
||||
app.include_router(me_router)
|
||||
app.include_router(auth_router)
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
from app.models.api_client import ApiClient
|
||||
from app.models.auth_sync_state import AuthSyncState
|
||||
from app.models.company import Company
|
||||
from app.models.module import Module
|
||||
from app.models.permission import Permission
|
||||
from app.models.permission_group import PermissionGroup
|
||||
from app.models.permission_group_member import PermissionGroupMember
|
||||
from app.models.permission_group_permission import PermissionGroupPermission
|
||||
from app.models.role import Role
|
||||
from app.models.site import Site
|
||||
from app.models.site_role import SiteRole
|
||||
from app.models.system import System
|
||||
from app.models.user import User
|
||||
from app.models.user_scope_permission import UserScopePermission
|
||||
from app.models.user_site import UserSite
|
||||
|
||||
__all__ = [
|
||||
"ApiClient",
|
||||
"AuthSyncState",
|
||||
"Company",
|
||||
"Module",
|
||||
"Permission",
|
||||
"PermissionGroup",
|
||||
"PermissionGroupMember",
|
||||
"PermissionGroupPermission",
|
||||
"Role",
|
||||
"Site",
|
||||
"SiteRole",
|
||||
"System",
|
||||
"User",
|
||||
"UserScopePermission",
|
||||
"UserSite",
|
||||
]
|
||||
|
||||
21
app/models/auth_sync_state.py
Normal file
21
app/models/auth_sync_state.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, String, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class AuthSyncState(Base):
|
||||
__tablename__ = "auth_sync_state"
|
||||
__table_args__ = (UniqueConstraint("entity_type", "entity_id", name="uq_auth_sync_state_entity"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
entity_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
entity_id: Mapped[str] = mapped_column(UUID(as_uuid=False), nullable=False)
|
||||
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
source_version: Mapped[str | None] = mapped_column(String(255))
|
||||
last_error: Mapped[str | None] = mapped_column(String)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
@@ -13,7 +13,9 @@ class Company(Base):
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
company_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
legal_name: Mapped[str | None] = mapped_column(String(255))
|
||||
idp_group_id: Mapped[str | None] = mapped_column(String(128))
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
__tablename__ = "permissions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"user_id",
|
||||
"scope_type",
|
||||
"scope_id",
|
||||
"module",
|
||||
"action",
|
||||
name="uq_permissions_user_scope_module_action",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
user_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
scope_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
scope_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
module: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
@@ -1,20 +0,0 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class PermissionGroupMember(Base):
|
||||
__tablename__ = "permission_group_members"
|
||||
__table_args__ = (UniqueConstraint("group_id", "user_sub", name="uq_permission_group_members_group_sub"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
group_id: Mapped[str] = mapped_column(
|
||||
UUID(as_uuid=False), ForeignKey("permission_groups.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
user_sub: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
@@ -1,23 +0,0 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class PermissionGroupPermission(Base):
|
||||
__tablename__ = "permission_group_permissions"
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
group_id: Mapped[str] = mapped_column(
|
||||
UUID(as_uuid=False), ForeignKey("permission_groups.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
system: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
module: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
scope_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
scope_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
@@ -1,22 +1,23 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Module(Base):
|
||||
__tablename__ = "modules"
|
||||
class Role(Base):
|
||||
__tablename__ = "roles"
|
||||
__table_args__ = (UniqueConstraint("system_id", "idp_role_name", name="uq_roles_system_idp_role_name"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
system_key: Mapped[str] = mapped_column(
|
||||
String(128), ForeignKey("systems.system_key", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
module_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
role_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
system_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("systems.id", ondelete="CASCADE"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(String(1024))
|
||||
idp_role_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
@@ -14,7 +14,9 @@ class Site(Base):
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
site_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
company_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
domain: Mapped[str | None] = mapped_column(String(255))
|
||||
idp_group_id: Mapped[str | None] = mapped_column(String(128))
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
|
||||
18
app/models/site_role.py
Normal file
18
app/models/site_role.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class SiteRole(Base):
|
||||
__tablename__ = "site_roles"
|
||||
__table_args__ = (UniqueConstraint("site_id", "role_id", name="uq_site_roles_site_role"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
site_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("sites.id", ondelete="CASCADE"), nullable=False)
|
||||
role_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("roles.id", ondelete="CASCADE"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
@@ -14,6 +14,7 @@ class System(Base):
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
system_key: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
idp_client_id: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
|
||||
@@ -13,12 +13,12 @@ class User(Base):
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
user_sub: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
idp_user_id: Mapped[str | None] = mapped_column(String(128))
|
||||
idp_user_id: Mapped[str | None] = mapped_column(String(128), unique=True)
|
||||
username: Mapped[str | None] = mapped_column(String(255), unique=True)
|
||||
email: Mapped[str | None] = mapped_column(String(320))
|
||||
email: Mapped[str | None] = mapped_column(String(320), unique=True)
|
||||
display_name: Mapped[str | None] = mapped_column(String(255))
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class UserScopePermission(Base):
|
||||
__tablename__ = "user_scope_permissions"
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
user_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
module_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
scope_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
company_id: Mapped[str | None] = mapped_column(UUID(as_uuid=False), ForeignKey("companies.id", ondelete="CASCADE"))
|
||||
site_id: Mapped[str | None] = mapped_column(UUID(as_uuid=False), ForeignKey("sites.id", ondelete="CASCADE"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||
)
|
||||
@@ -1,20 +1,20 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import DateTime, String, func
|
||||
from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class PermissionGroup(Base):
|
||||
__tablename__ = "permission_groups"
|
||||
class UserSite(Base):
|
||||
__tablename__ = "user_sites"
|
||||
__table_args__ = (UniqueConstraint("user_id", "site_id", name="uq_user_sites_user_site"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||
group_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
user_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
site_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("sites.id", ondelete="CASCADE"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||
94
app/repositories/api_clients_repo.py
Normal file
94
app/repositories/api_clients_repo.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.api_client import ApiClient
|
||||
|
||||
|
||||
class ApiClientsRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, client_key: str) -> ApiClient | None:
|
||||
return self.db.scalar(select(ApiClient).where(ApiClient.client_key == client_key))
|
||||
|
||||
def list(self, *, keyword: str | None = None, status: str | None = None, limit: int = 100, offset: int = 0) -> tuple[list[ApiClient], int]:
|
||||
stmt = select(ApiClient)
|
||||
count_stmt = select(func.count()).select_from(ApiClient)
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
cond = or_(ApiClient.client_key.ilike(pattern), ApiClient.name.ilike(pattern))
|
||||
stmt = stmt.where(cond)
|
||||
count_stmt = count_stmt.where(cond)
|
||||
if status:
|
||||
stmt = stmt.where(ApiClient.status == status)
|
||||
count_stmt = count_stmt.where(ApiClient.status == status)
|
||||
|
||||
stmt = stmt.order_by(ApiClient.created_at.desc()).limit(limit).offset(offset)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
client_key: str,
|
||||
name: str,
|
||||
status: str,
|
||||
api_key_hash: str,
|
||||
allowed_origins: list[str],
|
||||
allowed_ips: list[str],
|
||||
allowed_paths: list[str],
|
||||
rate_limit_per_min: int | None,
|
||||
expires_at,
|
||||
) -> ApiClient:
|
||||
item = ApiClient(
|
||||
client_key=client_key,
|
||||
name=name,
|
||||
status=status,
|
||||
api_key_hash=api_key_hash,
|
||||
allowed_origins=allowed_origins,
|
||||
allowed_ips=allowed_ips,
|
||||
allowed_paths=allowed_paths,
|
||||
rate_limit_per_min=rate_limit_per_min,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def update(
|
||||
self,
|
||||
item: ApiClient,
|
||||
*,
|
||||
name: str | None = None,
|
||||
status: str | None = None,
|
||||
api_key_hash: str | None = None,
|
||||
allowed_origins: list[str] | None = None,
|
||||
allowed_ips: list[str] | None = None,
|
||||
allowed_paths: list[str] | None = None,
|
||||
rate_limit_per_min: int | None = None,
|
||||
expires_at=None,
|
||||
) -> ApiClient:
|
||||
if name is not None:
|
||||
item.name = name
|
||||
if status is not None:
|
||||
item.status = status
|
||||
if api_key_hash is not None:
|
||||
item.api_key_hash = api_key_hash
|
||||
if allowed_origins is not None:
|
||||
item.allowed_origins = allowed_origins
|
||||
if allowed_ips is not None:
|
||||
item.allowed_ips = allowed_ips
|
||||
if allowed_paths is not None:
|
||||
item.allowed_paths = allowed_paths
|
||||
if rate_limit_per_min is not None:
|
||||
item.rate_limit_per_min = rate_limit_per_min
|
||||
if expires_at is not None:
|
||||
item.expires_at = expires_at
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def delete(self, item: ApiClient) -> None:
|
||||
self.db.delete(item)
|
||||
self.db.commit()
|
||||
@@ -9,36 +9,55 @@ class CompaniesRepository:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, company_key: str) -> Company | None:
|
||||
stmt = select(Company).where(Company.company_key == company_key)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(Company).where(Company.company_key == company_key))
|
||||
|
||||
def get_by_id(self, company_id: str) -> Company | None:
|
||||
stmt = select(Company).where(Company.id == company_id)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(Company).where(Company.id == company_id))
|
||||
|
||||
def list(self, keyword: str | None = None, limit: int = 100, offset: int = 0) -> tuple[list[Company], int]:
|
||||
stmt = select(Company)
|
||||
count_stmt = select(func.count()).select_from(Company)
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
cond = or_(Company.company_key.ilike(pattern), Company.name.ilike(pattern))
|
||||
cond = or_(
|
||||
Company.company_key.ilike(pattern),
|
||||
Company.display_name.ilike(pattern),
|
||||
Company.legal_name.ilike(pattern),
|
||||
)
|
||||
stmt = stmt.where(cond)
|
||||
count_stmt = count_stmt.where(cond)
|
||||
|
||||
stmt = stmt.order_by(Company.created_at.desc()).limit(limit).offset(offset)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(self, company_key: str, name: str, status: str = "active") -> Company:
|
||||
item = Company(company_key=company_key, name=name, status=status)
|
||||
def create(self, *, company_key: str, display_name: str, legal_name: str | None, status: str = "active") -> Company:
|
||||
item = Company(company_key=company_key, display_name=display_name, legal_name=legal_name, status=status)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def update(self, item: Company, *, name: str | None = None, status: str | None = None) -> Company:
|
||||
if name is not None:
|
||||
item.name = name
|
||||
def update(
|
||||
self,
|
||||
item: Company,
|
||||
*,
|
||||
display_name: str | None = None,
|
||||
legal_name: str | None = None,
|
||||
idp_group_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> Company:
|
||||
if display_name is not None:
|
||||
item.display_name = display_name
|
||||
if legal_name is not None:
|
||||
item.legal_name = legal_name
|
||||
if idp_group_id is not None:
|
||||
item.idp_group_id = idp_group_id
|
||||
if status is not None:
|
||||
item.status = status
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def delete(self, item: Company) -> None:
|
||||
self.db.delete(item)
|
||||
self.db.commit()
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.module import Module
|
||||
|
||||
|
||||
class ModulesRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, module_key: str) -> Module | None:
|
||||
stmt = select(Module).where(Module.module_key == module_key)
|
||||
return self.db.scalar(stmt)
|
||||
|
||||
def list(self, limit: int = 200, offset: int = 0) -> tuple[list[Module], int]:
|
||||
stmt = select(Module)
|
||||
count_stmt = select(func.count()).select_from(Module)
|
||||
stmt = stmt.order_by(Module.created_at.desc()).limit(limit).offset(offset)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(self, module_key: str, system_key: str, name: str, status: str = "active") -> Module:
|
||||
item = Module(module_key=module_key, system_key=system_key, name=name, status=status)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def update(self, item: Module, *, name: str | None = None, status: str | None = None) -> Module:
|
||||
if name is not None:
|
||||
item.name = name
|
||||
if status is not None:
|
||||
item.status = status
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
@@ -1,269 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.permission_group import PermissionGroup
|
||||
from app.models.permission_group_member import PermissionGroupMember
|
||||
from app.models.permission_group_permission import PermissionGroupPermission
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class PermissionGroupsRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, group_key: str) -> PermissionGroup | None:
|
||||
return self.db.scalar(select(PermissionGroup).where(PermissionGroup.group_key == group_key))
|
||||
|
||||
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)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(self, group_key: str, name: str, status: str = "active") -> PermissionGroup:
|
||||
item = PermissionGroup(group_key=group_key, name=name, status=status)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def update(self, item: PermissionGroup, *, name: str | None = None, status: str | None = None) -> PermissionGroup:
|
||||
if name is not None:
|
||||
item.name = name
|
||||
if status is not None:
|
||||
item.status = status
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def add_member_if_not_exists(self, group_id: str, user_sub: str) -> PermissionGroupMember:
|
||||
existing = self.db.scalar(
|
||||
select(PermissionGroupMember).where(
|
||||
PermissionGroupMember.group_id == group_id, PermissionGroupMember.user_sub == user_sub
|
||||
)
|
||||
)
|
||||
if existing:
|
||||
return existing
|
||||
row = PermissionGroupMember(group_id=group_id, user_sub=user_sub)
|
||||
self.db.add(row)
|
||||
self.db.commit()
|
||||
self.db.refresh(row)
|
||||
return row
|
||||
|
||||
def remove_member(self, group_id: str, user_sub: str) -> int:
|
||||
result = self.db.execute(
|
||||
delete(PermissionGroupMember).where(
|
||||
PermissionGroupMember.group_id == group_id, PermissionGroupMember.user_sub == user_sub
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
return int(result.rowcount or 0)
|
||||
|
||||
def list_group_keys_by_member_sub(self, user_sub: str) -> list[str]:
|
||||
stmt = (
|
||||
select(PermissionGroup.group_key)
|
||||
.select_from(PermissionGroupMember)
|
||||
.join(PermissionGroup, PermissionGroup.id == PermissionGroupMember.group_id)
|
||||
.where(PermissionGroupMember.user_sub == user_sub)
|
||||
.order_by(PermissionGroup.group_key.asc())
|
||||
)
|
||||
return [row[0] for row in self.db.execute(stmt).all()]
|
||||
|
||||
def replace_member_groups(self, user_sub: str, group_ids: list[str]) -> None:
|
||||
self.db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.user_sub == user_sub))
|
||||
for group_id in group_ids:
|
||||
self.db.add(PermissionGroupMember(group_id=group_id, user_sub=user_sub))
|
||||
self.db.commit()
|
||||
|
||||
def grant_group_permission(
|
||||
self,
|
||||
group_id: str,
|
||||
system: str,
|
||||
module: str,
|
||||
action: str,
|
||||
scope_type: str,
|
||||
scope_id: str,
|
||||
) -> PermissionGroupPermission:
|
||||
where = [
|
||||
PermissionGroupPermission.group_id == group_id,
|
||||
PermissionGroupPermission.system == system,
|
||||
PermissionGroupPermission.module == module,
|
||||
PermissionGroupPermission.action == action,
|
||||
PermissionGroupPermission.scope_type == scope_type,
|
||||
PermissionGroupPermission.scope_id == scope_id,
|
||||
]
|
||||
existing = self.db.scalar(select(PermissionGroupPermission).where(*where))
|
||||
if existing:
|
||||
return existing
|
||||
row = PermissionGroupPermission(
|
||||
group_id=group_id,
|
||||
system=system,
|
||||
module=module,
|
||||
action=action,
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
self.db.add(row)
|
||||
self.db.commit()
|
||||
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 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]))
|
||||
|
||||
normalized_systems = set([s for s in system_keys if s])
|
||||
module_pairs = []
|
||||
for pair in module_keys:
|
||||
if "|" not in pair:
|
||||
continue
|
||||
system_key, module_key = pair.split("|", 1)
|
||||
if not system_key or not module_key:
|
||||
continue
|
||||
module_pairs.append((system_key, module_key))
|
||||
normalized_systems.add(system_key)
|
||||
|
||||
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, user_sub=sub))
|
||||
|
||||
for site_key in normalized_sites:
|
||||
for action in normalized_actions:
|
||||
for system_key in sorted(normalized_systems):
|
||||
module_names = [m for s, m in module_pairs if s == 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(
|
||||
{
|
||||
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.user_sub)
|
||||
.where(PermissionGroupMember.group_id == group_id)
|
||||
.order_by(PermissionGroupMember.user_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.user_sub == User.user_sub)
|
||||
.join(PermissionGroupPermission, PermissionGroupPermission.group_id == PermissionGroupMember.group_id)
|
||||
.where(PermissionGroupPermission.system == system_key)
|
||||
.order_by(User.email.asc(), User.user_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.user_sub == User.user_sub)
|
||||
.join(PermissionGroupPermission, PermissionGroupPermission.group_id == PermissionGroupMember.group_id)
|
||||
.where(PermissionGroupPermission.system == system_key, PermissionGroupPermission.module == module_name)
|
||||
.order_by(User.email.asc(), User.user_sub.asc())
|
||||
.distinct()
|
||||
)
|
||||
return list(self.db.scalars(stmt).all())
|
||||
|
||||
def revoke_group_permission(
|
||||
self,
|
||||
group_id: str,
|
||||
system: str,
|
||||
module: str,
|
||||
action: str,
|
||||
scope_type: str,
|
||||
scope_id: str,
|
||||
) -> int:
|
||||
stmt = delete(PermissionGroupPermission).where(
|
||||
PermissionGroupPermission.group_id == group_id,
|
||||
PermissionGroupPermission.system == system,
|
||||
PermissionGroupPermission.module == module,
|
||||
PermissionGroupPermission.action == action,
|
||||
PermissionGroupPermission.scope_type == scope_type,
|
||||
PermissionGroupPermission.scope_id == scope_id,
|
||||
)
|
||||
result = self.db.execute(stmt)
|
||||
self.db.commit()
|
||||
return int(result.rowcount or 0)
|
||||
@@ -1,231 +0,0 @@
|
||||
from sqlalchemy import and_, delete, func, literal, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.company import Company
|
||||
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
|
||||
|
||||
|
||||
class PermissionsRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def list_by_user(self, user_id: str, user_sub: str) -> list[tuple[str, str, str | None, str, str]]:
|
||||
direct_stmt = (
|
||||
select(
|
||||
literal("direct"),
|
||||
UserScopePermission.scope_type,
|
||||
Company.company_key,
|
||||
Site.site_key,
|
||||
Module.system_key,
|
||||
Module.module_key,
|
||||
UserScopePermission.action,
|
||||
)
|
||||
.select_from(UserScopePermission)
|
||||
.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)
|
||||
.where(UserScopePermission.user_id == user_id)
|
||||
.where(UserScopePermission.action.in_(["view", "edit"]))
|
||||
.where(UserScopePermission.scope_type == "site")
|
||||
)
|
||||
group_stmt = (
|
||||
select(
|
||||
literal("group"),
|
||||
PermissionGroupPermission.scope_type,
|
||||
PermissionGroupPermission.scope_id,
|
||||
PermissionGroupPermission.system,
|
||||
PermissionGroupPermission.module,
|
||||
PermissionGroupPermission.action,
|
||||
)
|
||||
.select_from(PermissionGroupPermission)
|
||||
.join(PermissionGroupMember, PermissionGroupMember.group_id == PermissionGroupPermission.group_id)
|
||||
.where(PermissionGroupMember.user_sub == user_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()
|
||||
result: list[tuple[str, str, str | None, str, str]] = []
|
||||
dedup = set()
|
||||
for row in rows:
|
||||
source = row[0]
|
||||
if source == "group":
|
||||
_, scope_type, scope_id, system_key, module_key, action = row
|
||||
if module_key == "__system__":
|
||||
module_key = f"__system__{system_key}"
|
||||
else:
|
||||
_, scope_type, company_key, site_key, system_key, module_key, action = row
|
||||
scope_id = company_key if scope_type == "company" else site_key
|
||||
key = (scope_type, scope_id or "", system_key, module_key, action)
|
||||
if key in dedup:
|
||||
continue
|
||||
dedup.add(key)
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
def create_if_not_exists(
|
||||
self,
|
||||
user_id: str,
|
||||
module_id: str,
|
||||
action: str,
|
||||
scope_type: str,
|
||||
company_id: str | None,
|
||||
site_id: str | None,
|
||||
) -> UserScopePermission:
|
||||
where_expr = [
|
||||
UserScopePermission.user_id == user_id,
|
||||
UserScopePermission.module_id == module_id,
|
||||
UserScopePermission.action == action,
|
||||
UserScopePermission.scope_type == scope_type,
|
||||
]
|
||||
if scope_type == "company":
|
||||
where_expr.append(UserScopePermission.company_id == company_id)
|
||||
else:
|
||||
where_expr.append(UserScopePermission.site_id == site_id)
|
||||
|
||||
existing = self.db.scalar(select(UserScopePermission).where(and_(*where_expr)))
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
item = UserScopePermission(
|
||||
user_id=user_id,
|
||||
module_id=module_id,
|
||||
action=action,
|
||||
scope_type=scope_type,
|
||||
company_id=company_id,
|
||||
site_id=site_id,
|
||||
)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def revoke(
|
||||
self,
|
||||
user_id: str,
|
||||
module_id: str,
|
||||
action: str,
|
||||
scope_type: str,
|
||||
company_id: str | None,
|
||||
site_id: str | None,
|
||||
) -> int:
|
||||
stmt = delete(UserScopePermission).where(
|
||||
UserScopePermission.user_id == user_id,
|
||||
UserScopePermission.module_id == module_id,
|
||||
UserScopePermission.action == action,
|
||||
UserScopePermission.scope_type == scope_type,
|
||||
or_(
|
||||
and_(scope_type == "company", UserScopePermission.company_id == company_id),
|
||||
and_(scope_type == "site", UserScopePermission.site_id == site_id),
|
||||
),
|
||||
)
|
||||
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.user_sub,
|
||||
User.email,
|
||||
User.display_name,
|
||||
UserScopePermission.scope_type,
|
||||
Company.company_key,
|
||||
Site.site_key,
|
||||
Module.system_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)
|
||||
.where(UserScopePermission.action.in_(["view", "edit"]))
|
||||
.where(UserScopePermission.scope_type == "site")
|
||||
)
|
||||
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)
|
||||
.where(UserScopePermission.action.in_(["view", "edit"]))
|
||||
.where(UserScopePermission.scope_type == "site")
|
||||
)
|
||||
|
||||
if scope_type == "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.user_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,
|
||||
user_sub,
|
||||
email,
|
||||
display_name,
|
||||
row_scope_type,
|
||||
company_key,
|
||||
site_key,
|
||||
system_key,
|
||||
module_key,
|
||||
action,
|
||||
created_at,
|
||||
) = row
|
||||
scope_id = company_key if row_scope_type == "company" else site_key
|
||||
module_name = module_key
|
||||
if isinstance(module_name, str) and module_name.startswith("__system__"):
|
||||
module_name = None
|
||||
items.append(
|
||||
{
|
||||
"permission_id": permission_id,
|
||||
"user_sub": user_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)
|
||||
97
app/repositories/roles_repo.py
Normal file
97
app/repositories/roles_repo.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.role import Role
|
||||
|
||||
|
||||
class RolesRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, role_key: str) -> Role | None:
|
||||
return self.db.scalar(select(Role).where(Role.role_key == role_key))
|
||||
|
||||
def get_by_id(self, role_id: str) -> Role | None:
|
||||
return self.db.scalar(select(Role).where(Role.id == role_id))
|
||||
|
||||
def list(
|
||||
self,
|
||||
*,
|
||||
keyword: str | None = None,
|
||||
system_id: str | None = None,
|
||||
status: str | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[Role], int]:
|
||||
stmt = select(Role)
|
||||
count_stmt = select(func.count()).select_from(Role)
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
cond = or_(
|
||||
Role.role_key.ilike(pattern),
|
||||
Role.name.ilike(pattern),
|
||||
Role.idp_role_name.ilike(pattern),
|
||||
Role.description.ilike(pattern),
|
||||
)
|
||||
stmt = stmt.where(cond)
|
||||
count_stmt = count_stmt.where(cond)
|
||||
if system_id:
|
||||
stmt = stmt.where(Role.system_id == system_id)
|
||||
count_stmt = count_stmt.where(Role.system_id == system_id)
|
||||
if status:
|
||||
stmt = stmt.where(Role.status == status)
|
||||
count_stmt = count_stmt.where(Role.status == status)
|
||||
|
||||
stmt = stmt.order_by(Role.created_at.desc()).limit(limit).offset(offset)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
role_key: str,
|
||||
system_id: str,
|
||||
name: str,
|
||||
description: str | None,
|
||||
idp_role_name: str,
|
||||
status: str = "active",
|
||||
) -> Role:
|
||||
item = Role(
|
||||
role_key=role_key,
|
||||
system_id=system_id,
|
||||
name=name,
|
||||
description=description,
|
||||
idp_role_name=idp_role_name,
|
||||
status=status,
|
||||
)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def update(
|
||||
self,
|
||||
item: Role,
|
||||
*,
|
||||
system_id: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
idp_role_name: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> Role:
|
||||
if system_id is not None:
|
||||
item.system_id = system_id
|
||||
if name is not None:
|
||||
item.name = name
|
||||
if description is not None:
|
||||
item.description = description
|
||||
if idp_role_name is not None:
|
||||
item.idp_role_name = idp_role_name
|
||||
if status is not None:
|
||||
item.status = status
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def delete(self, item: Role) -> None:
|
||||
self.db.delete(item)
|
||||
self.db.commit()
|
||||
37
app/repositories/site_roles_repo.py
Normal file
37
app/repositories/site_roles_repo.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.role import Role
|
||||
from app.models.site import Site
|
||||
from app.models.site_role import SiteRole
|
||||
from app.models.system import System
|
||||
|
||||
|
||||
class SiteRolesRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def list_site_role_rows(self, site_id: str) -> list[tuple[SiteRole, Role, System]]:
|
||||
stmt = (
|
||||
select(SiteRole, Role, System)
|
||||
.join(Role, Role.id == SiteRole.role_id)
|
||||
.join(System, System.id == Role.system_id)
|
||||
.where(SiteRole.site_id == site_id)
|
||||
.order_by(System.name.asc(), Role.name.asc())
|
||||
)
|
||||
return list(self.db.execute(stmt).all())
|
||||
|
||||
def list_role_site_rows(self, role_id: str) -> list[tuple[SiteRole, Site]]:
|
||||
stmt = (
|
||||
select(SiteRole, Site)
|
||||
.join(Site, Site.id == SiteRole.site_id)
|
||||
.where(SiteRole.role_id == role_id)
|
||||
.order_by(Site.display_name.asc())
|
||||
)
|
||||
return list(self.db.execute(stmt).all())
|
||||
|
||||
def set_site_roles(self, *, site_id: str, role_ids: list[str]) -> None:
|
||||
self.db.execute(delete(SiteRole).where(SiteRole.site_id == site_id))
|
||||
for role_id in role_ids:
|
||||
self.db.add(SiteRole(site_id=site_id, role_id=role_id))
|
||||
self.db.commit()
|
||||
@@ -9,11 +9,14 @@ class SitesRepository:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, site_key: str) -> Site | None:
|
||||
stmt = select(Site).where(Site.site_key == site_key)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(Site).where(Site.site_key == site_key))
|
||||
|
||||
def get_by_id(self, site_id: str) -> Site | None:
|
||||
return self.db.scalar(select(Site).where(Site.id == site_id))
|
||||
|
||||
def list(
|
||||
self,
|
||||
*,
|
||||
keyword: str | None = None,
|
||||
company_id: str | None = None,
|
||||
limit: int = 100,
|
||||
@@ -21,19 +24,30 @@ class SitesRepository:
|
||||
) -> tuple[list[Site], int]:
|
||||
stmt = select(Site)
|
||||
count_stmt = select(func.count()).select_from(Site)
|
||||
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
cond = or_(Site.site_key.ilike(pattern), Site.name.ilike(pattern))
|
||||
cond = or_(Site.site_key.ilike(pattern), Site.display_name.ilike(pattern), Site.domain.ilike(pattern))
|
||||
stmt = stmt.where(cond)
|
||||
count_stmt = count_stmt.where(cond)
|
||||
|
||||
if company_id:
|
||||
stmt = stmt.where(Site.company_id == company_id)
|
||||
count_stmt = count_stmt.where(Site.company_id == company_id)
|
||||
|
||||
stmt = stmt.order_by(Site.created_at.desc()).limit(limit).offset(offset)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(self, site_key: str, company_id: str, name: str, status: str = "active") -> Site:
|
||||
item = Site(site_key=site_key, company_id=company_id, name=name, status=status)
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
site_key: str,
|
||||
company_id: str,
|
||||
display_name: str,
|
||||
domain: str | None,
|
||||
status: str = "active",
|
||||
) -> Site:
|
||||
item = Site(site_key=site_key, company_id=company_id, display_name=display_name, domain=domain, status=status)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
@@ -44,15 +58,25 @@ class SitesRepository:
|
||||
item: Site,
|
||||
*,
|
||||
company_id: str | None = None,
|
||||
name: str | None = None,
|
||||
display_name: str | None = None,
|
||||
domain: str | None = None,
|
||||
idp_group_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> Site:
|
||||
if company_id is not None:
|
||||
item.company_id = company_id
|
||||
if name is not None:
|
||||
item.name = name
|
||||
if display_name is not None:
|
||||
item.display_name = display_name
|
||||
if domain is not None:
|
||||
item.domain = domain
|
||||
if idp_group_id is not None:
|
||||
item.idp_group_id = idp_group_id
|
||||
if status is not None:
|
||||
item.status = status
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def delete(self, item: Site) -> None:
|
||||
self.db.delete(item)
|
||||
self.db.commit()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.system import System
|
||||
@@ -9,34 +9,51 @@ class SystemsRepository:
|
||||
self.db = db
|
||||
|
||||
def get_by_key(self, system_key: str) -> System | None:
|
||||
stmt = select(System).where(System.system_key == system_key)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(System).where(System.system_key == system_key))
|
||||
|
||||
def get_by_id(self, system_id: str) -> System | None:
|
||||
stmt = select(System).where(System.id == system_id)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(System).where(System.id == system_id))
|
||||
|
||||
def list(self, status: str | None = None, limit: int = 100, offset: int = 0) -> tuple[list[System], int]:
|
||||
def list(self, *, keyword: str | None = None, status: str | None = None, limit: int = 100, offset: int = 0) -> tuple[list[System], int]:
|
||||
stmt = select(System)
|
||||
count_stmt = select(func.count()).select_from(System)
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
cond = or_(System.system_key.ilike(pattern), System.name.ilike(pattern), System.idp_client_id.ilike(pattern))
|
||||
stmt = stmt.where(cond)
|
||||
count_stmt = count_stmt.where(cond)
|
||||
if status:
|
||||
stmt = stmt.where(System.status == status)
|
||||
count_stmt = count_stmt.where(System.status == status)
|
||||
|
||||
stmt = stmt.order_by(System.created_at.desc()).limit(limit).offset(offset)
|
||||
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||
|
||||
def create(self, system_key: str, name: str, status: str = "active") -> System:
|
||||
item = System(system_key=system_key, name=name, status=status)
|
||||
def create(self, *, system_key: str, name: str, idp_client_id: str, status: str = "active") -> System:
|
||||
item = System(system_key=system_key, name=name, idp_client_id=idp_client_id, status=status)
|
||||
self.db.add(item)
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def update(self, item: System, *, name: str | None = None, status: str | None = None) -> System:
|
||||
def update(
|
||||
self,
|
||||
item: System,
|
||||
*,
|
||||
name: str | None = None,
|
||||
idp_client_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> System:
|
||||
if name is not None:
|
||||
item.name = name
|
||||
if idp_client_id is not None:
|
||||
item.idp_client_id = idp_client_id
|
||||
if status is not None:
|
||||
item.status = status
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def delete(self, item: System) -> None:
|
||||
self.db.delete(item)
|
||||
self.db.commit()
|
||||
|
||||
54
app/repositories/user_sites_repo.py
Normal file
54
app/repositories/user_sites_repo.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.company import Company
|
||||
from app.models.role import Role
|
||||
from app.models.site import Site
|
||||
from app.models.site_role import SiteRole
|
||||
from app.models.system import System
|
||||
from app.models.user import User
|
||||
from app.models.user_site import UserSite
|
||||
|
||||
|
||||
class UserSitesRepository:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
def list_user_site_rows(self, user_id: str) -> list[tuple[UserSite, Site, Company]]:
|
||||
stmt = (
|
||||
select(UserSite, Site, Company)
|
||||
.join(Site, Site.id == UserSite.site_id)
|
||||
.join(Company, Company.id == Site.company_id)
|
||||
.where(UserSite.user_id == user_id)
|
||||
.order_by(Company.display_name.asc(), Site.display_name.asc())
|
||||
)
|
||||
return list(self.db.execute(stmt).all())
|
||||
|
||||
def list_site_member_rows(self, site_id: str) -> list[tuple[UserSite, User]]:
|
||||
stmt = (
|
||||
select(UserSite, User)
|
||||
.join(User, User.id == UserSite.user_id)
|
||||
.where(UserSite.site_id == site_id)
|
||||
.order_by(User.display_name.asc().nulls_last(), User.username.asc().nulls_last(), User.user_sub.asc())
|
||||
)
|
||||
return list(self.db.execute(stmt).all())
|
||||
|
||||
def set_user_sites(self, *, user_id: str, site_ids: list[str]) -> None:
|
||||
self.db.execute(delete(UserSite).where(UserSite.user_id == user_id))
|
||||
for site_id in site_ids:
|
||||
self.db.add(UserSite(user_id=user_id, site_id=site_id))
|
||||
self.db.commit()
|
||||
|
||||
def get_user_role_rows(self, user_id: str) -> list[tuple[Site, Company, Role, System]]:
|
||||
stmt = (
|
||||
select(Site, Company, Role, System)
|
||||
.select_from(UserSite)
|
||||
.join(Site, Site.id == UserSite.site_id)
|
||||
.join(Company, Company.id == Site.company_id)
|
||||
.join(SiteRole, SiteRole.site_id == Site.id)
|
||||
.join(Role, Role.id == SiteRole.role_id)
|
||||
.join(System, System.id == Role.system_id)
|
||||
.where(UserSite.user_id == user_id)
|
||||
.order_by(Company.display_name.asc(), Site.display_name.asc(), System.name.asc(), Role.name.asc())
|
||||
)
|
||||
return list(self.db.execute(stmt).all())
|
||||
@@ -9,15 +9,14 @@ class UsersRepository:
|
||||
self.db = db
|
||||
|
||||
def get_by_sub(self, user_sub: str) -> User | None:
|
||||
stmt = select(User).where(User.user_sub == user_sub)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(User).where(User.user_sub == user_sub))
|
||||
|
||||
def get_by_id(self, user_id: str) -> User | None:
|
||||
stmt = select(User).where(User.id == user_id)
|
||||
return self.db.scalar(stmt)
|
||||
return self.db.scalar(select(User).where(User.id == user_id))
|
||||
|
||||
def list(
|
||||
self,
|
||||
*,
|
||||
keyword: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
limit: int = 50,
|
||||
@@ -48,11 +47,13 @@ class UsersRepository:
|
||||
|
||||
def upsert_by_sub(
|
||||
self,
|
||||
*,
|
||||
user_sub: str,
|
||||
username: str | None,
|
||||
email: str | None,
|
||||
display_name: str | None,
|
||||
is_active: bool,
|
||||
status: str = "active",
|
||||
idp_user_id: str | None = None,
|
||||
) -> User:
|
||||
user = self.get_by_sub(user_sub)
|
||||
@@ -64,6 +65,7 @@ class UsersRepository:
|
||||
email=email,
|
||||
display_name=display_name,
|
||||
is_active=is_active,
|
||||
status=status,
|
||||
)
|
||||
self.db.add(user)
|
||||
else:
|
||||
@@ -73,6 +75,7 @@ class UsersRepository:
|
||||
user.email = email
|
||||
user.display_name = display_name
|
||||
user.is_active = is_active
|
||||
user.status = status
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
@@ -86,6 +89,7 @@ class UsersRepository:
|
||||
email: str | None = None,
|
||||
display_name: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
status: str | None = None,
|
||||
) -> User:
|
||||
if username is not None:
|
||||
user.username = username
|
||||
@@ -95,6 +99,13 @@ class UsersRepository:
|
||||
user.display_name = display_name
|
||||
if is_active is not None:
|
||||
user.is_active = is_active
|
||||
if status is not None:
|
||||
user.status = status
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
return user
|
||||
|
||||
def delete(self, user: User) -> None:
|
||||
self.db.delete(user)
|
||||
self.db.commit()
|
||||
|
||||
@@ -1,113 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SystemCreateRequest(BaseModel):
|
||||
name: str
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class SystemUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class SystemItem(BaseModel):
|
||||
id: str
|
||||
system_key: str
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class ModuleCreateRequest(BaseModel):
|
||||
system_key: str
|
||||
name: str
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class ModuleUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class ModuleItem(BaseModel):
|
||||
id: str
|
||||
system_key: str | None = None
|
||||
module_key: str
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class CompanyCreateRequest(BaseModel):
|
||||
name: str
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class CompanyUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class CompanyItem(BaseModel):
|
||||
id: str
|
||||
company_key: str
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class SiteCreateRequest(BaseModel):
|
||||
company_key: str
|
||||
name: str
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class SiteUpdateRequest(BaseModel):
|
||||
company_key: str | None = None
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class SiteItem(BaseModel):
|
||||
id: str
|
||||
site_key: str
|
||||
company_key: str
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class MemberItem(BaseModel):
|
||||
id: str
|
||||
user_sub: str
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool
|
||||
|
||||
|
||||
class MemberUpsertRequest(BaseModel):
|
||||
user_sub: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool = True
|
||||
sync_to_idp: bool = True
|
||||
|
||||
|
||||
class MemberUpdateRequest(BaseModel):
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool | None = None
|
||||
sync_to_idp: bool = True
|
||||
|
||||
|
||||
class MemberPasswordResetResponse(BaseModel):
|
||||
user_sub: str
|
||||
temporary_password: str
|
||||
|
||||
|
||||
class MemberPermissionGroupsUpdateRequest(BaseModel):
|
||||
group_keys: list[str]
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ListResponse(BaseModel):
|
||||
@@ -117,62 +10,217 @@ class ListResponse(BaseModel):
|
||||
offset: int
|
||||
|
||||
|
||||
class PermissionGroupCreateRequest(BaseModel):
|
||||
name: str
|
||||
class CompanyCreateRequest(BaseModel):
|
||||
display_name: str
|
||||
legal_name: str | None = None
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class PermissionGroupUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
class CompanyUpdateRequest(BaseModel):
|
||||
display_name: str | None = None
|
||||
legal_name: str | None = None
|
||||
idp_group_id: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class PermissionGroupItem(BaseModel):
|
||||
class CompanyItem(BaseModel):
|
||||
id: str
|
||||
group_key: str
|
||||
company_key: str
|
||||
display_name: str
|
||||
legal_name: str | None = None
|
||||
idp_group_id: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
class SiteCreateRequest(BaseModel):
|
||||
company_key: str
|
||||
display_name: str
|
||||
domain: str | None = None
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class SiteUpdateRequest(BaseModel):
|
||||
company_key: str | None = None
|
||||
display_name: str | None = None
|
||||
domain: str | None = None
|
||||
idp_group_id: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class SiteItem(BaseModel):
|
||||
id: str
|
||||
site_key: str
|
||||
company_key: str
|
||||
company_display_name: str
|
||||
display_name: str
|
||||
domain: str | None = None
|
||||
idp_group_id: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
class SystemCreateRequest(BaseModel):
|
||||
name: str
|
||||
status: str
|
||||
idp_client_id: str
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class PermissionGroupPermissionItem(BaseModel):
|
||||
class SystemUpdateRequest(BaseModel):
|
||||
name: str | None = None
|
||||
idp_client_id: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class SystemItem(BaseModel):
|
||||
id: str
|
||||
system: str
|
||||
module: str
|
||||
action: Literal["view", "edit"]
|
||||
scope_type: Literal["site"]
|
||||
scope_id: str
|
||||
|
||||
|
||||
class MemberPermissionGroupsResponse(BaseModel):
|
||||
user_sub: 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
|
||||
system_key: str
|
||||
name: str
|
||||
idp_client_id: str
|
||||
status: str
|
||||
|
||||
|
||||
class MemberRelationItem(BaseModel):
|
||||
class RoleCreateRequest(BaseModel):
|
||||
system_key: str
|
||||
name: str
|
||||
idp_role_name: str
|
||||
description: str | None = None
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class RoleUpdateRequest(BaseModel):
|
||||
system_key: str | None = None
|
||||
name: str | None = None
|
||||
idp_role_name: str | None = None
|
||||
description: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class RoleItem(BaseModel):
|
||||
id: str
|
||||
role_key: str
|
||||
system_key: str
|
||||
system_name: str
|
||||
name: str
|
||||
idp_role_name: str
|
||||
description: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
class MemberItem(BaseModel):
|
||||
id: str
|
||||
user_sub: str
|
||||
idp_user_id: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool
|
||||
status: str
|
||||
|
||||
|
||||
class MemberUpsertRequest(BaseModel):
|
||||
user_sub: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool = True
|
||||
status: str = "active"
|
||||
sync_to_idp: bool = True
|
||||
|
||||
|
||||
class MemberUpdateRequest(BaseModel):
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool | None = None
|
||||
status: str | None = None
|
||||
sync_to_idp: bool = True
|
||||
|
||||
|
||||
class MemberPasswordResetResponse(BaseModel):
|
||||
user_sub: str
|
||||
temporary_password: str
|
||||
|
||||
|
||||
class SiteRoleAssignRequest(BaseModel):
|
||||
role_keys: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SiteRoleItem(BaseModel):
|
||||
id: str
|
||||
role_key: str
|
||||
role_name: str
|
||||
system_key: str
|
||||
system_name: str
|
||||
|
||||
|
||||
class UserSiteAssignRequest(BaseModel):
|
||||
site_keys: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class UserSiteItem(BaseModel):
|
||||
id: str
|
||||
site_key: str
|
||||
site_display_name: str
|
||||
company_key: str
|
||||
company_display_name: str
|
||||
|
||||
|
||||
class UserEffectiveRoleItem(BaseModel):
|
||||
site_key: str
|
||||
site_display_name: str
|
||||
company_key: str
|
||||
company_display_name: str
|
||||
system_key: str
|
||||
system_name: str
|
||||
role_key: str
|
||||
role_name: str
|
||||
idp_role_name: str
|
||||
|
||||
|
||||
class UserEffectiveRolesResponse(BaseModel):
|
||||
user_sub: str
|
||||
roles: list[UserEffectiveRoleItem]
|
||||
|
||||
|
||||
class SiteMembersResponse(BaseModel):
|
||||
site_key: str
|
||||
members: list[MemberItem]
|
||||
|
||||
|
||||
class SiteRolesResponse(BaseModel):
|
||||
site_key: str
|
||||
roles: list[SiteRoleItem]
|
||||
|
||||
|
||||
class UserSitesResponse(BaseModel):
|
||||
user_sub: str
|
||||
sites: list[UserSiteItem]
|
||||
|
||||
|
||||
class CompanySitesResponse(BaseModel):
|
||||
company_key: str
|
||||
sites: list[SiteItem]
|
||||
|
||||
|
||||
class SystemRolesResponse(BaseModel):
|
||||
system_key: str
|
||||
roles: list[RoleItem]
|
||||
|
||||
|
||||
class RoleSitesResponse(BaseModel):
|
||||
role_key: str
|
||||
sites: list[UserSiteItem]
|
||||
|
||||
|
||||
class ApiClientItem(BaseModel):
|
||||
id: str
|
||||
client_key: str
|
||||
name: str
|
||||
status: str
|
||||
allowed_origins: list[str] = Field(default_factory=list)
|
||||
allowed_ips: list[str] = Field(default_factory=list)
|
||||
allowed_paths: list[str] = Field(default_factory=list)
|
||||
rate_limit_per_min: int | None = None
|
||||
expires_at: datetime | None = None
|
||||
last_used_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -5,6 +5,7 @@ class InternalSystemItem(BaseModel):
|
||||
id: str
|
||||
system_key: str
|
||||
name: str
|
||||
idp_client_id: str
|
||||
status: str
|
||||
|
||||
|
||||
@@ -15,16 +16,19 @@ class InternalSystemListResponse(BaseModel):
|
||||
offset: int
|
||||
|
||||
|
||||
class InternalModuleItem(BaseModel):
|
||||
class InternalRoleItem(BaseModel):
|
||||
id: str
|
||||
module_key: str
|
||||
role_key: str
|
||||
system_key: str
|
||||
system_name: str
|
||||
name: str
|
||||
idp_role_name: str
|
||||
description: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
class InternalModuleListResponse(BaseModel):
|
||||
items: list[InternalModuleItem]
|
||||
class InternalRoleListResponse(BaseModel):
|
||||
items: list[InternalRoleItem]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
@@ -33,7 +37,8 @@ class InternalModuleListResponse(BaseModel):
|
||||
class InternalCompanyItem(BaseModel):
|
||||
id: str
|
||||
company_key: str
|
||||
name: str
|
||||
display_name: str
|
||||
legal_name: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
@@ -47,8 +52,10 @@ class InternalCompanyListResponse(BaseModel):
|
||||
class InternalSiteItem(BaseModel):
|
||||
id: str
|
||||
site_key: str
|
||||
company_key: str | None = None
|
||||
name: str
|
||||
company_key: str
|
||||
company_display_name: str
|
||||
display_name: str
|
||||
domain: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
@@ -66,6 +73,7 @@ class InternalMemberItem(BaseModel):
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool
|
||||
status: str
|
||||
|
||||
|
||||
class InternalMemberListResponse(BaseModel):
|
||||
@@ -83,3 +91,21 @@ class InternalUpsertUserBySubResponse(BaseModel):
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool
|
||||
status: str
|
||||
|
||||
|
||||
class InternalUserRoleItem(BaseModel):
|
||||
site_key: str
|
||||
site_display_name: str
|
||||
company_key: str
|
||||
company_display_name: str
|
||||
system_key: str
|
||||
system_name: str
|
||||
role_key: str
|
||||
role_name: str
|
||||
idp_role_name: str
|
||||
|
||||
|
||||
class InternalUserRoleResponse(BaseModel):
|
||||
user_sub: str
|
||||
roles: list[InternalUserRoleItem]
|
||||
|
||||
@@ -1,60 +1,18 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
ActionType = Literal["view", "edit"]
|
||||
ScopeType = Literal["site"]
|
||||
|
||||
class RoleSnapshotItem(BaseModel):
|
||||
site_key: str
|
||||
site_display_name: str
|
||||
company_key: str
|
||||
company_display_name: str
|
||||
system_key: str
|
||||
system_name: str
|
||||
role_key: str
|
||||
role_name: str
|
||||
idp_role_name: str
|
||||
|
||||
|
||||
class PermissionGrantRequest(BaseModel):
|
||||
class RoleSnapshotResponse(BaseModel):
|
||||
user_sub: str
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
scope_type: ScopeType
|
||||
scope_id: str
|
||||
system: str
|
||||
module: str | None = None
|
||||
action: ActionType
|
||||
|
||||
|
||||
class PermissionRevokeRequest(BaseModel):
|
||||
user_sub: str
|
||||
scope_type: ScopeType
|
||||
scope_id: str
|
||||
system: str
|
||||
module: str | None = None
|
||||
action: ActionType
|
||||
|
||||
|
||||
class PermissionItem(BaseModel):
|
||||
scope_type: ScopeType
|
||||
scope_id: str
|
||||
system: str | None = None
|
||||
module: str
|
||||
action: ActionType
|
||||
|
||||
|
||||
class PermissionSnapshotResponse(BaseModel):
|
||||
user_sub: str
|
||||
permissions: list[PermissionItem]
|
||||
|
||||
|
||||
class DirectPermissionRow(BaseModel):
|
||||
permission_id: str
|
||||
user_sub: str
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
scope_type: ScopeType
|
||||
scope_id: str
|
||||
system: str | None = None
|
||||
module: str | None = None
|
||||
action: ActionType
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class DirectPermissionListResponse(BaseModel):
|
||||
items: list[DirectPermissionRow]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
roles: list[RoleSnapshotItem]
|
||||
|
||||
@@ -7,3 +7,4 @@ class UserUpsertBySubRequest(BaseModel):
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
is_active: bool = True
|
||||
status: str = "active"
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
from app.schemas.permissions import PermissionItem, PermissionSnapshotResponse
|
||||
from app.schemas.permissions import RoleSnapshotItem, RoleSnapshotResponse
|
||||
|
||||
|
||||
class PermissionService:
|
||||
@staticmethod
|
||||
def build_snapshot(user_sub: str, permissions: list[tuple[str, str, str | None, str, str]]) -> PermissionSnapshotResponse:
|
||||
return PermissionSnapshotResponse(
|
||||
def build_role_snapshot(user_sub: str, rows: list[tuple[str, str, str, str, str, str, str, str, str]]) -> RoleSnapshotResponse:
|
||||
return RoleSnapshotResponse(
|
||||
user_sub=user_sub,
|
||||
permissions=[
|
||||
PermissionItem(scope_type=s_type, scope_id=s_id, system=system, module=module, action=action)
|
||||
for s_type, s_id, system, module, action in permissions
|
||||
roles=[
|
||||
RoleSnapshotItem(
|
||||
site_key=site_key,
|
||||
site_display_name=site_display_name,
|
||||
company_key=company_key,
|
||||
company_display_name=company_display_name,
|
||||
system_key=system_key,
|
||||
system_name=system_name,
|
||||
role_key=role_key,
|
||||
role_name=role_name,
|
||||
idp_role_name=idp_role_name,
|
||||
)
|
||||
for (
|
||||
site_key,
|
||||
site_display_name,
|
||||
company_key,
|
||||
company_display_name,
|
||||
system_key,
|
||||
system_name,
|
||||
role_key,
|
||||
role_name,
|
||||
idp_role_name,
|
||||
) in rows
|
||||
],
|
||||
)
|
||||
|
||||
@@ -2,25 +2,29 @@ BEGIN;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- Drop all managed tables to ensure clean rebuild
|
||||
-- Drop legacy/managed tables for clean rebuild
|
||||
DROP TABLE IF EXISTS auth_sync_state CASCADE;
|
||||
DROP TABLE IF EXISTS user_sites CASCADE;
|
||||
DROP TABLE IF EXISTS site_roles CASCADE;
|
||||
DROP TABLE IF EXISTS roles CASCADE;
|
||||
DROP TABLE IF EXISTS api_clients CASCADE;
|
||||
DROP TABLE IF EXISTS sites CASCADE;
|
||||
DROP TABLE IF EXISTS companies CASCADE;
|
||||
DROP TABLE IF EXISTS systems CASCADE;
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
|
||||
-- legacy tables
|
||||
DROP TABLE IF EXISTS permissions CASCADE;
|
||||
DROP TABLE IF EXISTS modules CASCADE;
|
||||
DROP TABLE IF EXISTS user_scope_permissions CASCADE;
|
||||
DROP TABLE IF EXISTS permission_group_permissions CASCADE;
|
||||
DROP TABLE IF EXISTS permission_group_members CASCADE;
|
||||
DROP TABLE IF EXISTS permission_groups CASCADE;
|
||||
DROP TABLE IF EXISTS modules CASCADE;
|
||||
DROP TABLE IF EXISTS systems CASCADE;
|
||||
DROP TABLE IF EXISTS sites CASCADE;
|
||||
DROP TABLE IF EXISTS companies CASCADE;
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
DROP TABLE IF EXISTS api_clients CASCADE;
|
||||
-- remove legacy table if present
|
||||
DROP TABLE IF EXISTS permissions CASCADE;
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_sub TEXT NOT NULL UNIQUE,
|
||||
idp_user_id VARCHAR(128),
|
||||
idp_user_id VARCHAR(128) UNIQUE,
|
||||
username TEXT UNIQUE,
|
||||
email TEXT UNIQUE,
|
||||
display_name TEXT,
|
||||
@@ -30,18 +34,12 @@ CREATE TABLE users (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE auth_sync_state (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
last_synced_at TIMESTAMPTZ,
|
||||
source_version TEXT,
|
||||
last_error TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE companies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
company_key TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
legal_name TEXT,
|
||||
idp_group_id TEXT,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
@@ -51,7 +49,9 @@ CREATE TABLE sites (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
site_key TEXT NOT NULL UNIQUE,
|
||||
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
domain TEXT,
|
||||
idp_group_id TEXT,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
@@ -61,67 +61,51 @@ CREATE TABLE systems (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
system_key TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
idp_client_id TEXT NOT NULL UNIQUE,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE modules (
|
||||
CREATE TABLE roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
system_key TEXT NOT NULL REFERENCES systems(system_key) ON DELETE CASCADE,
|
||||
module_key TEXT NOT NULL UNIQUE,
|
||||
role_key TEXT NOT NULL UNIQUE,
|
||||
system_id UUID NOT NULL REFERENCES systems(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
idp_role_name TEXT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- direct permission table retained only for compatibility
|
||||
CREATE TABLE user_scope_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
scope_type VARCHAR(16) NOT NULL,
|
||||
company_id UUID REFERENCES companies(id) ON DELETE CASCADE,
|
||||
site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT user_scope_permissions_scope_check
|
||||
CHECK (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL),
|
||||
CONSTRAINT user_scope_permissions_action_check
|
||||
CHECK (action IN ('view', 'edit'))
|
||||
CONSTRAINT uq_roles_system_idp_role_name UNIQUE (system_id, idp_role_name)
|
||||
);
|
||||
|
||||
CREATE TABLE permission_groups (
|
||||
CREATE TABLE site_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_key TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
CONSTRAINT uq_site_roles_site_role UNIQUE (site_id, role_id)
|
||||
);
|
||||
|
||||
CREATE TABLE permission_group_members (
|
||||
CREATE TABLE user_sites (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||
user_sub TEXT NOT NULL,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_permission_group_members_group_sub UNIQUE (group_id, user_sub)
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_user_sites_user_site UNIQUE (user_id, site_id)
|
||||
);
|
||||
|
||||
CREATE TABLE permission_group_permissions (
|
||||
CREATE TABLE auth_sync_state (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||
system TEXT NOT NULL,
|
||||
module TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT permission_group_permissions_scope_check CHECK (scope_type = 'site'),
|
||||
CONSTRAINT permission_group_permissions_action_check CHECK (action IN ('view', 'edit')),
|
||||
CONSTRAINT uq_pgp_group_rule UNIQUE (group_id, system, module, action, scope_type, scope_id)
|
||||
entity_type VARCHAR(32) NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
last_synced_at TIMESTAMPTZ,
|
||||
source_version TEXT,
|
||||
last_error TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_auth_sync_state_entity UNIQUE (entity_type, entity_id)
|
||||
);
|
||||
|
||||
CREATE TABLE api_clients (
|
||||
@@ -140,26 +124,18 @@ CREATE TABLE api_clients (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO systems (system_key, name, status)
|
||||
VALUES ('member', 'Member Center', 'active')
|
||||
ON CONFLICT (system_key) DO NOTHING;
|
||||
|
||||
CREATE INDEX idx_users_user_sub ON users(user_sub);
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_sites_company_id ON sites(company_id);
|
||||
CREATE INDEX idx_usp_user_id ON user_scope_permissions(user_id);
|
||||
CREATE INDEX idx_usp_module_id ON user_scope_permissions(module_id);
|
||||
CREATE INDEX idx_usp_site_id ON user_scope_permissions(site_id);
|
||||
CREATE UNIQUE INDEX uq_usp_site
|
||||
ON user_scope_permissions(user_id, module_id, action, scope_type, site_id);
|
||||
CREATE INDEX idx_pgm_group_id ON permission_group_members(group_id);
|
||||
CREATE INDEX idx_pgm_user_sub ON permission_group_members(user_sub);
|
||||
CREATE INDEX idx_pgp_group_id ON permission_group_permissions(group_id);
|
||||
CREATE INDEX idx_pgp_scope_site ON permission_group_permissions(scope_id);
|
||||
CREATE INDEX idx_roles_system_id ON roles(system_id);
|
||||
CREATE INDEX idx_site_roles_site_id ON site_roles(site_id);
|
||||
CREATE INDEX idx_site_roles_role_id ON site_roles(role_id);
|
||||
CREATE INDEX idx_user_sites_user_id ON user_sites(user_id);
|
||||
CREATE INDEX idx_user_sites_site_id ON user_sites(site_id);
|
||||
CREATE INDEX idx_auth_sync_entity ON auth_sync_state(entity_type, entity_id);
|
||||
CREATE INDEX idx_api_clients_status ON api_clients(status);
|
||||
CREATE INDEX idx_api_clients_expires_at ON api_clients(expires_at);
|
||||
CREATE INDEX idx_systems_system_key ON systems(system_key);
|
||||
CREATE INDEX idx_modules_system_key ON modules(system_key);
|
||||
CREATE INDEX idx_modules_module_key ON modules(module_key);
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS idp_user_id VARCHAR(128);
|
||||
@@ -1,16 +0,0 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS username TEXT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'uq_users_username'
|
||||
) THEN
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT uq_users_username UNIQUE (username);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
@@ -1,73 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'record_status') THEN
|
||||
CREATE TYPE record_status AS ENUM ('active','inactive');
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS systems (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
system_key TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
status record_status NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO systems (system_key, name, status)
|
||||
VALUES ('member', 'Member Center', 'active')
|
||||
ON CONFLICT (system_key) DO NOTHING;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'scope_type') THEN
|
||||
CREATE TYPE scope_type AS ENUM ('company','site');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'permission_action') THEN
|
||||
CREATE TYPE permission_action AS ENUM ('view','create','update','delete','manage');
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS permission_groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_key TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
status record_status NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS permission_group_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||
user_sub TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_permission_group_members_group_sub UNIQUE (group_id, user_sub)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS permission_group_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||
system TEXT NOT NULL,
|
||||
module TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
scope_type TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_systems_system_key ON systems(system_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_pgm_group_id ON permission_group_members(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pgm_user_sub ON permission_group_members(user_sub);
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_group_id ON permission_group_permissions(group_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_pgp_group_rule
|
||||
ON permission_group_permissions(group_id, system, module, action, scope_type, scope_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -1,27 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
-- users / master tables
|
||||
ALTER TABLE users ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
ALTER TABLE companies ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
ALTER TABLE sites ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
ALTER TABLE systems ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
ALTER TABLE modules ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
ALTER TABLE permission_groups ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
|
||||
-- api_clients
|
||||
ALTER TABLE api_clients ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
|
||||
|
||||
-- user scoped permissions
|
||||
ALTER TABLE user_scope_permissions ALTER COLUMN action TYPE VARCHAR(32) USING action::text;
|
||||
ALTER TABLE user_scope_permissions ALTER COLUMN scope_type TYPE VARCHAR(16) USING scope_type::text;
|
||||
|
||||
-- keep check constraint compatible with varchar
|
||||
ALTER TABLE user_scope_permissions DROP CONSTRAINT IF EXISTS user_scope_permissions_check;
|
||||
ALTER TABLE user_scope_permissions
|
||||
ADD CONSTRAINT user_scope_permissions_check
|
||||
CHECK (
|
||||
((scope_type = 'company' AND company_id IS NOT NULL AND site_id IS NULL)
|
||||
OR (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL))
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -1,6 +0,0 @@
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN idp_user_id TYPE VARCHAR(128)
|
||||
USING CASE
|
||||
WHEN idp_user_id IS NULL THEN NULL
|
||||
ELSE idp_user_id::text
|
||||
END;
|
||||
@@ -1,43 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'idp_sub'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'user_sub'
|
||||
) THEN
|
||||
ALTER TABLE users RENAME COLUMN idp_sub TO user_sub;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'idp_user_id'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'idp_user_id'
|
||||
) THEN
|
||||
ALTER TABLE users RENAME COLUMN idp_user_id TO idp_user_id;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'permission_group_members' AND column_name = 'idp_sub'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'permission_group_members' AND column_name = 'user_sub'
|
||||
) THEN
|
||||
ALTER TABLE permission_group_members RENAME COLUMN idp_sub TO user_sub;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
ALTER INDEX IF EXISTS idx_users_idp_sub RENAME TO idx_users_user_sub;
|
||||
ALTER INDEX IF EXISTS idx_pgm_idp_sub RENAME TO idx_pgm_user_sub;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_user_sub ON users(user_sub);
|
||||
CREATE INDEX IF NOT EXISTS idx_pgm_user_sub ON permission_group_members(user_sub);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user