동의 게이트와 런타임 안정화

This commit is contained in:
Yun Chan 2026-06-27 17:22:38 +09:00
parent 0eb7d925ed
commit 0ec266a761
34 changed files with 1186 additions and 158 deletions

View file

@ -25,7 +25,14 @@ from fastapi import APIRouter, Cookie, HTTPException, Query, Request, Response,
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
from ..auth_sessions import InactiveUserError, SessionUser, create_session, revoke_session
from ..auth_sessions import (
InactiveUserError,
SessionUser,
create_session,
record_user_consent,
revoke_session,
withdraw_user_consent,
)
from ..config import settings
from ..deps import CurrentPrincipal, Principal, Role
from ..saml import (
@ -71,6 +78,15 @@ class MeResponse(BaseModel):
display_name: str
role: str
cohort_ids: list[str]
consent_at: float | None = None
class ConsentRequest(BaseModel):
accepted: bool = True
class ConsentResponse(BaseModel):
consent_at: float | None = None
class AuthProviderStatus(BaseModel):
@ -564,6 +580,7 @@ def _me_response(user: SessionUser | Principal) -> MeResponse:
display_name=getattr(user, "display_name", "") or getattr(user, "email", ""),
role=user.role.value if isinstance(user.role, Role) else user.role,
cohort_ids=user.cohort_ids,
consent_at=getattr(user, "consent_at", None),
)
@ -933,6 +950,35 @@ async def logout(
return {"ok": True}
@router.post("/consent", response_model=ConsentResponse)
async def accept_consent(
body: ConsentRequest,
principal: CurrentPrincipal,
) -> ConsentResponse:
"""Record the current learner's practice-session consent receipt."""
if principal.role != Role.LEARNER:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="learner consent only")
if not body.accepted:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="consent_not_accepted")
consent_at = await record_user_consent(principal.user_id)
if consent_at is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="user not found")
principal.consent_at = consent_at
return ConsentResponse(consent_at=consent_at)
@router.delete("/consent", response_model=ConsentResponse)
async def withdraw_consent(principal: CurrentPrincipal) -> ConsentResponse:
"""Withdraw practice-session consent until the learner accepts again."""
if principal.role != Role.LEARNER:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="learner consent only")
changed = await withdraw_user_consent(principal.user_id)
if not changed:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="user not found")
principal.consent_at = None
return ConsentResponse(consent_at=None)
@router.get("/me", response_model=MeResponse)
async def me(principal: CurrentPrincipal) -> MeResponse:
"""Return the current authenticated user. Unauthenticated requests are 401."""