You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

107 lines
4.2 KiB
Python

# backend/app/routers/connectors.py — phase-13 외부 연동 API
# prefix 없음. main.py 에서 include_router(prefix="/api").
import uuid
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import RedirectResponse
from sqlmodel import Session
from ..config import get_settings
from ..connectors import oauth as oauthlib
from ..connectors.registry import ConnectorRegistry
from ..db import get_session
from ..models import ConnectorAccount, ConnectorAccountLog, ConnState
from ..schemas import ConnectorStatusOut, ImportResultOut, OAuthStartOut, SyncResultOut
router = APIRouter()
@router.get("/connectors", response_model=list[ConnectorStatusOut])
def list_connectors(domain: str | None = None, s: Session = Depends(get_session)):
rows = ConnectorRegistry.accounts(s, domain)
return [ConnectorStatusOut.from_account(a, ConnectorRegistry.mode_for(a.domain)) for a in rows]
@router.post("/connectors/{account_id}/sync", response_model=SyncResultOut)
def sync_one(account_id: str, full: bool = False, s: Session = Depends(get_session)):
a = s.get(ConnectorAccount, account_id)
if not a:
raise HTTPException(404, "connector account not found")
res = ConnectorRegistry.get(s, a).sync(s, full=full)
return SyncResultOut(**res.__dict__)
@router.post("/connectors/sync-all", response_model=list[SyncResultOut])
def sync_all(s: Session = Depends(get_session)):
out = []
for a in ConnectorRegistry.accounts(s):
if a.state == ConnState.disconnected: # 연결 안 된 계정은 건너뜀
continue
out.append(SyncResultOut(**ConnectorRegistry.get(s, a).sync(s).__dict__))
return out
# ── OAuth ──
@router.get("/connectors/oauth/start", response_model=OAuthStartOut)
def oauth_start(
domain: str, provider: str, redirect_after: str = "/life", s: Session = Depends(get_session)
):
cfg = get_settings()
if provider in ("gmail", "google_calendar") and not cfg.google_client_id:
raise HTTPException(400, "google_client_id 미설정 — real 연동 비활성")
if provider == "notion" and not cfg.notion_client_id:
raise HTTPException(400, "notion_client_id 미설정 — real 연동 비활성")
url = oauthlib.start_oauth(s, domain, provider, redirect_after)
return OAuthStartOut(authorize_url=url)
@router.get("/connectors/oauth/callback")
def oauth_callback(code: str, state: str, s: Session = Depends(get_session)):
origin = get_settings().frontend_origin
try:
acct = oauthlib.finish_oauth(s, code, state)
except Exception:
return RedirectResponse(url=f"{origin}/life?connect=error")
try:
ConnectorRegistry.get(s, acct).sync(s, full=True) # 연결 직후 1회 초기 sync
except Exception:
pass
return RedirectResponse(url=f"{origin}/life?connect=ok&domain={acct.domain.value}")
@router.post("/connectors/{account_id}/disconnect", response_model=ConnectorStatusOut)
def disconnect(account_id: str, s: Session = Depends(get_session)):
a = s.get(ConnectorAccount, account_id)
if not a:
raise HTTPException(404, "not found")
a.state = ConnState.disconnected
a.token_enc = ""
a.last_label = "연결 안 됨"
s.add(a)
s.add(
ConnectorAccountLog(
id="cl-" + uuid.uuid4().hex[:8],
account_id=a.id,
action="disconnected",
detail="사용자 해제",
)
)
s.commit()
return ConnectorStatusOut.from_account(a, ConnectorRegistry.mode_for(a.domain))
# ── 로컬 우선: 수동 임포트(CSV / .ics / HealthKit export) ──
@router.post("/connectors/{account_id}/import", response_model=ImportResultOut)
async def import_file(
account_id: str, file: UploadFile = File(...), s: Session = Depends(get_session)
):
a = s.get(ConnectorAccount, account_id)
if not a:
raise HTTPException(404, "not found")
conn = ConnectorRegistry.get(s, a)
if not hasattr(conn, "import_bytes"):
raise HTTPException(400, "이 커넥터는 파일 임포트를 지원하지 않아요")
content = await file.read()
res = conn.import_bytes(s, content, filename=file.filename or "")
return ImportResultOut(**res)