feat: bootstrap backend MVP and architecture docs
This commit is contained in:
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API routers."""
|
||||
60
app/api/admin.py
Normal file
60
app/api/admin.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.models.api_client import ApiClient
|
||||
from app.repositories.permissions_repo import PermissionsRepository
|
||||
from app.repositories.users_repo import UsersRepository
|
||||
from app.schemas.permissions import PermissionGrantRequest, PermissionRevokeRequest
|
||||
from app.security.api_client_auth import require_api_client
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
@router.post("/permissions/grant")
|
||||
def grant_permission(
|
||||
payload: PermissionGrantRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, str]:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
|
||||
user = users_repo.upsert_by_sub(
|
||||
authentik_sub=payload.authentik_sub,
|
||||
email=payload.email,
|
||||
display_name=payload.display_name,
|
||||
is_active=True,
|
||||
)
|
||||
permission = perms_repo.create_if_not_exists(
|
||||
user_id=user.id,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
module=payload.module,
|
||||
action=payload.action,
|
||||
)
|
||||
|
||||
return {"permission_id": permission.id, "result": "granted"}
|
||||
|
||||
|
||||
@router.post("/permissions/revoke")
|
||||
def revoke_permission(
|
||||
payload: PermissionRevokeRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, int | str]:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
|
||||
user = users_repo.get_by_sub(payload.authentik_sub)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
|
||||
|
||||
deleted = perms_repo.revoke(
|
||||
user_id=user.id,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
module=payload.module,
|
||||
action=payload.action,
|
||||
)
|
||||
return {"deleted": deleted, "result": "revoked"}
|
||||
60
app/api/internal.py
Normal file
60
app/api/internal.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
||||
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.repositories.users_repo import UsersRepository
|
||||
from app.schemas.permissions import PermissionSnapshotResponse
|
||||
from app.schemas.users import UserUpsertBySubRequest
|
||||
from app.services.permission_service import PermissionService
|
||||
|
||||
router = APIRouter(prefix="/internal", tags=["internal"])
|
||||
|
||||
|
||||
def verify_internal_secret(x_internal_secret: str = Header(alias="X-Internal-Secret")) -> None:
|
||||
settings = get_settings()
|
||||
if not settings.internal_shared_secret:
|
||||
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="internal_secret_not_configured")
|
||||
if x_internal_secret != settings.internal_shared_secret:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_internal_secret")
|
||||
|
||||
|
||||
@router.post("/users/upsert-by-sub")
|
||||
def upsert_user_by_sub(
|
||||
payload: UserUpsertBySubRequest,
|
||||
_: None = Depends(verify_internal_secret),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, str | bool | None]:
|
||||
repo = UsersRepository(db)
|
||||
user = repo.upsert_by_sub(
|
||||
authentik_sub=payload.sub,
|
||||
email=payload.email,
|
||||
display_name=payload.display_name,
|
||||
is_active=payload.is_active,
|
||||
)
|
||||
return {
|
||||
"id": user.id,
|
||||
"sub": user.authentik_sub,
|
||||
"email": user.email,
|
||||
"display_name": user.display_name,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/permissions/{authentik_sub}/snapshot", response_model=PermissionSnapshotResponse)
|
||||
def get_permission_snapshot(
|
||||
authentik_sub: str,
|
||||
_: None = Depends(verify_internal_secret),
|
||||
db: Session = Depends(get_db),
|
||||
) -> PermissionSnapshotResponse:
|
||||
users_repo = UsersRepository(db)
|
||||
perms_repo = PermissionsRepository(db)
|
||||
|
||||
user = users_repo.get_by_sub(authentik_sub)
|
||||
if user is None:
|
||||
return PermissionSnapshotResponse(authentik_sub=authentik_sub, permissions=[])
|
||||
|
||||
permissions = perms_repo.list_by_user_id(user.id)
|
||||
tuples = [(p.scope_type, p.scope_id, p.module, p.action) for p in permissions]
|
||||
return PermissionService.build_snapshot(authentik_sub=authentik_sub, permissions=tuples)
|
||||
Reference in New Issue
Block a user