solution-site 빌드가 TS2440(Import declaration conflicts with local declaration of 'useCallback')로 실패했다. FastAPI 라우트 핸들러 함수명이 `callback`이라 orval이 만든 React Query 훅 이름이 `useCallback`이 됐고, 같은 파일에서 import한 React의 useCallback과 이름이 겹쳤다. - router/v1/social/oauth.py: 함수명 callback → oauth_callback - 백엔드 재빌드 후 npm run orval 로 재생성 → useOauthCallback, callbackParams.ts → oauthCallbackParams.ts docker compose build solution-site 로 재검증, 정상 빌드 확인
70 lines
2.8 KiB
Python
70 lines
2.8 KiB
Python
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 oauth_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}
|