from uuid import UUID from fastapi import APIRouter, Depends, Request, Response, HTTPException, Query from fastapi.responses import RedirectResponse from common.logger import LOG from common.models.gmodel import UserInfo from router.v1.validator.dependencies import IsValidAccessToken from services import social_account_service as service from services.external.social import SocialError router = APIRouter(prefix="/v1/social/oauth", tags=["Social"]) COOKIE = "social_oauth_browser" @router.post("/connect") async def connect(response: Response, user: UserInfo = Depends(IsValidAccessToken)): try: url, browser = service.begin(UUID(user.user_id), 2) except SocialError as ex: raise HTTPException(409, str(ex)) from ex response.set_cookie( COOKIE, browser, httponly=True, secure=True, samesite="lax", max_age=600, path="/v1/social/oauth", ) response.headers["Cache-Control"] = "no-store" return {"url": url} @router.get("/callback") async def callback( request: Request, state: str = Query("", max_length=2048), code: str = Query("", max_length=4096), error: str = Query("", max_length=200), ): ok = False if not error and code and state: try: await service.finish(state, request.cookies.get(COOKIE), code) ok = True except Exception as ex: # noqa: BLE001 # ★ 화면에는 원문을 내보내지 않는다 — OAuth 응답·state 에는 자격증명이 들어 있다. # 대신 **서버 로그에는 반드시 남긴다.** 예전에는 통째로 삼켜서, 연결이 안 될 때 # 화면에 `?social=failed` 만 뜨고 우리도 이유를 알 방법이 없었다 # (키가 틀렸는지 · 쿠키가 안 왔는지 · state 가 만료됐는지 구별이 안 된다). # ★ 남기는 것은 **예외 종류와 우리가 만든 사유 문자열**뿐이다. 토큰·code·state 는 찍지 않는다. LOG.w(f"[social] 계정 연결 실패: {type(ex).__name__}: {ex}") elif error: # 사장님이 Meta 화면에서 취소한 경우도 여기로 온다 — 고장과 구별되게 남긴다. LOG.i(f"[social] 계정 연결 중단(제공자 응답): {error[:80]}") response = RedirectResponse( "/sites?social=" + ("connected" if ok else "failed"), status_code=303 ) response.delete_cookie( COOKIE, path="/v1/social/oauth", secure=True, httponly=True, samesite="lax" ) response.headers["Cache-Control"] = "no-store" response.headers["Referrer-Policy"] = "no-referrer" return response @router.post("/disconnect") async def disconnect(user: UserInfo = Depends(IsValidAccessToken)): await service.disconnect(UUID(user.user_id), 2) return {"disconnected": True}