feat(auth): add /auth/login and /auth/callback OAuth2 routes to main.py

This commit is contained in:
chrischristiansen-glitch 2026-05-27 20:06:08 +02:00
parent 63f8bd289c
commit a729d0b345

81
main.py
View File

@ -5,6 +5,7 @@ Modes: light (gemini-2.5-flash) | heavy (gemini-2.5-pro)
ML-1: telemetri, state store, DAG. ML-1: telemetri, state store, DAG.
CG1+CG2: billing endpoints + IAP-beskyttelse. CG1+CG2: billing endpoints + IAP-beskyttelse.
CG3: static-mappe serveres fra /static/*. CG3: static-mappe serveres fra /static/*.
AUTH: /auth/login + /auth/callback for klient OAuth2 onboarding.
""" """
import os import os
@ -15,7 +16,7 @@ import pathlib
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agents", "core-logic")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agents", "core-logic"))
from fastapi import FastAPI, HTTPException, Request 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 fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List 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.telemetry import log_dag_execution
from ml.billing_agent import BillingAgent from ml.billing_agent import BillingAgent
from ml.anomaly_detector import AnomalyDetector 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") app = FastAPI(title="OSVauco OPAX Agent")
AGENT_ID = "opax-core" AGENT_ID = "opax-core"
# ── IAP-MIDDLEWARE ──────────────────────────────────────────────────────────── # ── 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_ENABLED = os.environ.get("IAP_ENABLED", "").lower() == "true"
IAP_AUDIENCE = os.environ.get("IAP_AUDIENCE", "") IAP_AUDIENCE = os.environ.get("IAP_AUDIENCE", "")
def _verify_iap_jwt(token: str, audience: str) -> dict: def _verify_iap_jwt(token: str, audience: str) -> dict:
"""Verifiserer IAP JWT med google-auth. Kaster ValueError ved feil."""
try: try:
from google.auth.transport import requests as google_requests from google.auth.transport import requests as google_requests
from google.oauth2 import id_token from google.oauth2 import id_token
@ -54,7 +50,6 @@ def _verify_iap_jwt(token: str, audience: str) -> dict:
@app.middleware("http") @app.middleware("http")
async def iap_guard(request: Request, call_next): 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"): if IAP_ENABLED and request.url.path.startswith("/billing"):
token = request.headers.get("X-Goog-IAP-JWT-Assertion", "") token = request.headers.get("X-Goog-IAP-JWT-Assertion", "")
if not token: if not token:
@ -68,7 +63,6 @@ async def iap_guard(request: Request, call_next):
# ── STATIC FILES ────────────────────────────────────────────────────────────── # ── STATIC FILES ──────────────────────────────────────────────────────────────
# Serverer static/ direkte: GET /static/billing-dashboard.html osv.
_static_dir = pathlib.Path(__file__).parent / "static" _static_dir = pathlib.Path(__file__).parent / "static"
if _static_dir.is_dir(): if _static_dir.is_dir():
app.mount("/static", StaticFiles(directory=str(_static_dir), html=True), name="static") app.mount("/static", StaticFiles(directory=str(_static_dir), html=True), name="static")
@ -96,27 +90,57 @@ def health():
return {"status": "ok"} 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=<klient-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) ──────────────────────────────────────────── # ── BILLING ENDPOINTS (CG1 + CG2) ────────────────────────────────────────────
@app.get("/billing/summary") @app.get("/billing/summary")
def billing_summary(): def billing_summary(client_id: str = ""):
try: try:
return BillingAgent().get_summary() return BillingAgent(client_id=client_id or None).get_summary()
except Exception as exc: except Exception as exc:
return JSONResponse(status_code=500, content={"error": str(exc)}) return JSONResponse(status_code=500, content={"error": str(exc)})
@app.get("/billing/forecast") @app.get("/billing/forecast")
def billing_forecast(): def billing_forecast(client_id: str = ""):
try: try:
return BillingAgent().get_forecast() return BillingAgent(client_id=client_id or None).get_forecast()
except Exception as exc: except Exception as exc:
return JSONResponse(status_code=500, content={"error": str(exc)}) return JSONResponse(status_code=500, content={"error": str(exc)})
@app.get("/billing/anomalies") @app.get("/billing/anomalies")
def billing_anomalies(): def billing_anomalies(client_id: str = ""):
try: try:
return AnomalyDetector().detect_anomalies() return AnomalyDetector(client_id=client_id or None).detect_anomalies()
except Exception as exc: except Exception as exc:
return JSONResponse(status_code=500, content={"error": str(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)} 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__": if __name__ == "__main__":
import uvicorn import uvicorn
port = int(os.environ.get("PORT", 8080)) port = int(os.environ.get("PORT", 8080))