first commit

This commit is contained in:
Chris
2026-03-23 20:23:58 +08:00
commit 74d612aca1
3193 changed files with 692056 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
from __future__ import annotations
from dataclasses import asdict
from fastapi import HTTPException, status
from app.domain.permissions import build_permission_context
from app.repositories.directus.client import DirectusClient
from app.schemas.auth import AuthenticatedUser, PermissionContextRead
class AuthContextService:
"""Validates Directus-issued tokens and normalizes user context.
FastAPI remains the formal API entrypoint, but Directus is still the source
of truth for login/session/role data. This service is the seam between them.
"""
# Keep the first auth bootstrap intentionally conservative.
# Directus `/users/me` on this project reliably returns the core user/role
# fields below, while deeper `user_group.*` expansions can collapse the
# payload back to only `{id}`. We can layer group/domain permissions back in
# later once the base admin flow is verified end to end.
me_fields = [
"email",
"first_name",
"id",
"role.id",
"role.name",
"status",
]
def __init__(self, client: DirectusClient | None = None) -> None:
self.client = client or DirectusClient()
async def get_authenticated_user(self, access_token: str) -> AuthenticatedUser:
if not access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing access token.",
)
me = await self.client.get_current_user(
access_token=access_token,
fields=self.me_fields,
)
if not me:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unable to resolve current user from Directus.",
)
user_group = me.get("user_group") or {}
permissions = user_group.get("domain_permissions") or []
normalized_permissions = [str(permission) for permission in permissions]
role = me.get("role") or {}
permission_context = build_permission_context(
role_name=role.get("name"),
domain_permissions=normalized_permissions,
)
# Keep the normalized payload explicit so downstream services do not need
# to know the exact nested Directus response shape.
return AuthenticatedUser.model_validate(
{
**me,
"domain_permissions": normalized_permissions,
"permissions": PermissionContextRead.model_validate(asdict(permission_context)),
}
)