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
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)
|