feat(engine): claude -p 상주 멀티턴 엔진 게이트웨이 + 실동작 검증
- engine_gateway/gateway.py: 회기당 claude -p 상주 프로세스(stream-json), 턴 직렬, budget 제한 - 세션 생성/턴/종료 HTTP API(FastAPI, :9099) - 검증: 멀티턴 컨텍스트 유지 + prompt caching 재사용(턴2 +$0.07) 실동작 확인
This commit is contained in:
parent
d5b86c5f89
commit
84eb6e2173
20 changed files with 2038 additions and 1 deletions
84
apps/api/app/routes/auth.py
Normal file
84
apps/api/app/routes/auth.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""인증 라우트 — BFF OAuth 2.1 Auth Code + PKCE(S256) 스텁.
|
||||
|
||||
마스터플랜 §5: BFF + OAuth 2.1, 토큰은 서버(Redis)에만, 브라우저엔 __Host- HttpOnly 쿠키.
|
||||
미성년 사례데이터 + 상담 민감정보 -> XSS 토큰탈취 원천 차단.
|
||||
1차 = Google OIDC 단독, 한신대 SSO 는 2차(R11, Authlib provider 추상화 뒤).
|
||||
|
||||
이 파일은 라우트 시그니처 + 흐름 + TODO. 실제 OAuth 교환/Redis 세션은 Phase 2 트랙 B.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..config import settings
|
||||
from ..deps import CurrentPrincipal
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
class MeResponse(BaseModel):
|
||||
user_id: str
|
||||
role: str
|
||||
cohort_ids: list[str]
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
async def login(
|
||||
provider: Annotated[str, Query()] = "google",
|
||||
) -> RedirectResponse:
|
||||
"""OAuth Auth Code + PKCE 시작 (BFF).
|
||||
|
||||
절차:
|
||||
1. code_verifier 생성 -> S256 code_challenge
|
||||
2. state(CSRF) + verifier 를 서버 세션(Redis)에 저장
|
||||
3. provider authorize URL 로 302 (Google OIDC 1차)
|
||||
TODO: Authlib provider 추상화 + Redis state 저장. 현재 스텁 501.
|
||||
"""
|
||||
if provider != "google":
|
||||
# 한신대 SSO 는 2차 (R11)
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=f"provider {provider} not yet supported")
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail="OAuth login TODO (Phase 2 트랙 B)")
|
||||
|
||||
|
||||
@router.get("/callback")
|
||||
async def callback(
|
||||
response: Response,
|
||||
code: Annotated[Optional[str], Query()] = None,
|
||||
state: Annotated[Optional[str], Query()] = None,
|
||||
) -> RedirectResponse:
|
||||
"""OAuth 콜백 — code -> token 교환 후 서버 세션 발급.
|
||||
|
||||
절차:
|
||||
1. state 검증 (Redis 의 저장값과 대조, CSRF)
|
||||
2. code + code_verifier 로 token 교환 (PKCE)
|
||||
3. id_token 검증 -> user upsert -> role/cohort 매핑
|
||||
4. Redis 세션 생성 -> __Host- HttpOnly Secure SameSite=Lax 쿠키 set
|
||||
5. IRB 동의 미이행 시 동의 게이트로 리다이렉트 (마스터플랜 §7)
|
||||
TODO: 전체 교환 구현. 현재 스텁 501.
|
||||
"""
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail="OAuth callback TODO (Phase 2 트랙 B)")
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(response: Response) -> dict[str, bool]:
|
||||
"""세션 무효화 (Redis 삭제 + 쿠키 만료). IRB 철회 즉시 무효화 경로 겸용.
|
||||
|
||||
TODO: Redis 세션 삭제. 현재 쿠키 만료만.
|
||||
"""
|
||||
response.delete_cookie(settings.cookie_name, httponly=True, secure=settings.is_prod, samesite="lax")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/me", response_model=MeResponse)
|
||||
async def me(principal: CurrentPrincipal) -> MeResponse:
|
||||
"""현재 세션 주체 (프론트 부트스트랩용). 미인증이면 deps 에서 401."""
|
||||
return MeResponse(
|
||||
user_id=principal.user_id,
|
||||
role=principal.role.value,
|
||||
cohort_ids=principal.cohort_ids,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue