"""카카오톡 채널 연결 — 빌더에서 코드를 받아 채널에 한 번 입력한다. ★ 소비(redeem) 엔드포인트는 여기 없다. 코드를 소비하는 쪽은 채널 웹훅이고, 그 웹훅은 자체 서명 검증을 갖춘 뒤에야 열 수 있다. 검증 없는 공개 소비 경로를 먼저 만들면 누구나 코드를 대입해 남의 계정에 자기 카톡을 붙일 수 있다 — 이 표가 막으려던 바로 그 일이다. """ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Response from common.models.gmodel import UserInfo from router.v1.validator.dependencies import IsValidAccessToken from services import kakao_link_service as service from services.kakao_link_service import KakaoLinkError router = APIRouter(prefix="/v1/agent/kakao", tags=["Agent"]) def private_response(response: Response): """코드가 오가는 응답이다 — 캐시·리퍼러·색인을 모두 막는다(social 라우터와 같은 규약).""" response.headers["Cache-Control"] = "no-store" response.headers["Referrer-Policy"] = "no-referrer" response.headers["X-Robots-Tag"] = "noindex, nofollow" @router.get("/link") async def link_state(response: Response, user: UserInfo = Depends(IsValidAccessToken)): """연결 상태. 사업장을 고르지 않아도 답할 수 있어야 하는 값이다 — 계정은 사람에 붙는다.""" private_response(response) return await service.state(UUID(user.user_id)) @router.post("/link/code") async def issue_code(response: Response, user: UserInfo = Depends(IsValidAccessToken)): """일회용 코드를 낸다. ★ 평문 코드는 이 응답에서 한 번만 나가고 DB 에는 sha256 만 남는다.""" private_response(response) try: return await service.issue_code(UUID(user.user_id)) except KakaoLinkError as ex: raise HTTPException(409, str(ex)) from ex @router.post("/link/disconnect") async def disconnect(response: Response, user: UserInfo = Depends(IsValidAccessToken)): private_response(response) try: await service.disconnect(UUID(user.user_id)) except KakaoLinkError as ex: raise HTTPException(409, str(ex)) from ex return {"disconnected": True}