diff --git a/main.py b/main.py index e7764d9..5e23b61 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ Modes: light (gemini-2.5-flash) | heavy (gemini-2.5-pro) ML-1: telemetri, state store, DAG. CG1+CG2: billing endpoints + IAP-beskyttelse. CG3: static-mappe serveres fra /static/*. +AUTH: /auth/login + /auth/callback for klient OAuth2 onboarding. """ import os @@ -15,7 +16,7 @@ import pathlib sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agents", "core-logic")) from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from typing import List @@ -25,23 +26,18 @@ from ml import build_agent_dag, execute_dag, get_store, log_agent_call from ml.telemetry import log_dag_execution from ml.billing_agent import BillingAgent from ml.anomaly_detector import AnomalyDetector +from auth.oauth_flow import get_authorization_url, exchange_code_for_token +from auth.token_store import save_token app = FastAPI(title="OSVauco OPAX Agent") AGENT_ID = "opax-core" # ── IAP-MIDDLEWARE ──────────────────────────────────────────────────────────── -# Cloud Run + IAP: verifiserer X-Goog-IAP-JWT-Assertion på /billing/*. -# Sett env-variabelen IAP_AUDIENCE til IAP backend service audience: -# /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID -# Sett IAP_ENABLED=true i Cloud Run for å aktivere sjekken. -# Lokalt (IAP_ENABLED ikke satt): alle kall tillates. - IAP_ENABLED = os.environ.get("IAP_ENABLED", "").lower() == "true" IAP_AUDIENCE = os.environ.get("IAP_AUDIENCE", "") def _verify_iap_jwt(token: str, audience: str) -> dict: - """Verifiserer IAP JWT med google-auth. Kaster ValueError ved feil.""" try: from google.auth.transport import requests as google_requests from google.oauth2 import id_token @@ -54,7 +50,6 @@ def _verify_iap_jwt(token: str, audience: str) -> dict: @app.middleware("http") async def iap_guard(request: Request, call_next): - """Blokkerer /billing/* uten gyldig IAP-token når IAP_ENABLED=true.""" if IAP_ENABLED and request.url.path.startswith("/billing"): token = request.headers.get("X-Goog-IAP-JWT-Assertion", "") if not token: @@ -68,7 +63,6 @@ async def iap_guard(request: Request, call_next): # ── STATIC FILES ────────────────────────────────────────────────────────────── -# Serverer static/ direkte: GET /static/billing-dashboard.html osv. _static_dir = pathlib.Path(__file__).parent / "static" if _static_dir.is_dir(): app.mount("/static", StaticFiles(directory=str(_static_dir), html=True), name="static") @@ -96,27 +90,57 @@ def health(): return {"status": "ok"} +# ── AUTH — OAuth2 klient-onboarding ────────────────────────────────────────── +@app.get("/auth/login") +def auth_login(client_id: str): + """ + Start OAuth2-flow for en klient. + Redirect klienten til: GET /auth/login?client_id= + """ + try: + auth_url = get_authorization_url(client_id) + return RedirectResponse(url=auth_url) + except Exception as exc: + return JSONResponse(status_code=500, content={"error": str(exc)}) + + +@app.get("/auth/callback") +def auth_callback(code: str, state: str): + """ + Google redirecter hit etter klient-godkjenning. + Bytter code mot token og lagrer i Secret Manager. + """ + try: + client_id, token_dict = exchange_code_for_token(code, state) + save_token(client_id, token_dict) + return RedirectResponse(url=f"/static/billing-dashboard.html?client_id={client_id}") + except ValueError as exc: + return JSONResponse(status_code=400, content={"error": str(exc)}) + except Exception as exc: + return JSONResponse(status_code=500, content={"error": str(exc)}) + + # ── BILLING ENDPOINTS (CG1 + CG2) ──────────────────────────────────────────── @app.get("/billing/summary") -def billing_summary(): +def billing_summary(client_id: str = ""): try: - return BillingAgent().get_summary() + return BillingAgent(client_id=client_id or None).get_summary() except Exception as exc: return JSONResponse(status_code=500, content={"error": str(exc)}) @app.get("/billing/forecast") -def billing_forecast(): +def billing_forecast(client_id: str = ""): try: - return BillingAgent().get_forecast() + return BillingAgent(client_id=client_id or None).get_forecast() except Exception as exc: return JSONResponse(status_code=500, content={"error": str(exc)}) @app.get("/billing/anomalies") -def billing_anomalies(): +def billing_anomalies(client_id: str = ""): try: - return AnomalyDetector().detect_anomalies() + return AnomalyDetector(client_id=client_id or None).detect_anomalies() except Exception as exc: return JSONResponse(status_code=500, content={"error": str(exc)}) @@ -237,31 +261,6 @@ def telemetry_history(limit: int = 50): return {"history": get_store().history(limit=limit)} -@app.get("/billing/summary") -def get_billing_summary(): - try: - agent = BillingAgent() - return agent.get_summary() - except Exception as e: - return {"error": str(e)} - -@app.get("/billing/forecast") -def get_billing_forecast(): - try: - agent = BillingAgent() - return agent.get_forecast() - except Exception as e: - return {"error": str(e)} - -@app.get("/billing/anomalies") -def get_billing_anomalies(): - try: - detector = AnomalyDetector() - return detector.detect_anomalies() - except Exception as e: - return {"error": str(e)} - - if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 8080))