feat(auth): add /auth/login and /auth/callback OAuth2 routes to main.py
This commit is contained in:
parent
63f8bd289c
commit
a729d0b345
81
main.py
81
main.py
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user