"""OAuth2 login/callback for Saxo — paths outside /api, matching the callback URL registered on the Saxo app dashboard (https://openfin.open-squared.tech/oauth/saxo/callback).""" import logging from fastapi import APIRouter, HTTPException, Query from fastapi.responses import RedirectResponse from services import saxo_auth logger = logging.getLogger(__name__) router = APIRouter(tags=["saxo-oauth"]) FRONTEND_CONFIG_URL = "/config" @router.get("/oauth/saxo/login") def saxo_login(): if not saxo_auth.is_configured(): raise HTTPException(status_code=400, detail="SAXO_APP_KEY / SAXO_APP_SECRET manquants dans backend/.env") state = saxo_auth.new_login_state() return RedirectResponse(saxo_auth.build_authorize_url(state)) @router.get("/oauth/saxo/callback") def saxo_callback(code: str = Query(...), state: str = Query(...)): if not saxo_auth.consume_and_verify_state(state): raise HTTPException(status_code=400, detail="État OAuth invalide (CSRF) — relancez la connexion depuis Config.") try: saxo_auth.exchange_code_for_tokens(code) except Exception as e: logger.error(f"[Saxo OAuth] Token exchange failed: {e}") return RedirectResponse(f"{FRONTEND_CONFIG_URL}?saxo_error=1") # Snapshot the whole watchlist right away — don't make the user wait for the next # scheduled poll after connecting. Runs in the background so the redirect isn't delayed. import threading from services.saxo_scheduler import run_snapshot_pass threading.Thread(target=run_snapshot_pass, daemon=True, name="saxo-connect-snapshot").start() return RedirectResponse(f"{FRONTEND_CONFIG_URL}?saxo_connected=1") @router.post("/oauth/saxo/refresh") def saxo_manual_refresh(): result = saxo_auth.refresh_tokens() if not result: raise HTTPException(status_code=400, detail="Saxo n'est pas connecté") return {"refreshed": True}