feat: webbasierte Azure-Authentifizierung
Device-Code-Flow direkt ueber das Web-Interface starten statt ueber die Kommandozeile. Button "Mit Azure verbinden" in den Einstellungen zeigt Code und Link an, pollt den Status und laedt die Seite nach Erfolg neu. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, Request, Form, UploadFile, File
|
||||
from fastapi import FastAPI, Request, Form, UploadFile, File, BackgroundTasks
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
@@ -859,6 +859,80 @@ async def delete_firmware(filename: str):
|
||||
return RedirectResponse("/devices", status_code=303)
|
||||
|
||||
|
||||
# ── Azure Auth (Web-basiert) ──────────────────────────────────────────────
|
||||
|
||||
# Global state for device code flow
|
||||
_auth_state = {"status": "idle", "user_code": None, "verification_uri": None, "error": None}
|
||||
|
||||
|
||||
def _run_device_code_auth():
|
||||
"""Background task: run device code flow and save auth record."""
|
||||
from azure.identity import DeviceCodeCredential, TokenCachePersistenceOptions
|
||||
from database import get_setting, set_setting
|
||||
|
||||
client_id = get_setting("azure_client_id")
|
||||
tenant_id = get_setting("azure_auth_tenant", "common")
|
||||
scopes = get_setting("azure_scopes", "User.Read").split()
|
||||
|
||||
if not client_id:
|
||||
_auth_state["status"] = "error"
|
||||
_auth_state["error"] = "Anwendungs-ID (Client) nicht konfiguriert"
|
||||
return
|
||||
|
||||
def prompt_callback(verification_uri, user_code, expires_on):
|
||||
_auth_state["user_code"] = user_code
|
||||
_auth_state["verification_uri"] = verification_uri
|
||||
_auth_state["status"] = "waiting"
|
||||
|
||||
try:
|
||||
cred = DeviceCodeCredential(
|
||||
client_id=client_id,
|
||||
tenant_id=tenant_id,
|
||||
cache_persistence_options=TokenCachePersistenceOptions(
|
||||
name="busylight", allow_unencrypted_storage=True
|
||||
),
|
||||
prompt_callback=prompt_callback,
|
||||
)
|
||||
record = cred.authenticate(tenant_id=tenant_id, scopes=scopes)
|
||||
set_setting("azure_auth_record", record.serialize())
|
||||
_auth_state["status"] = "success"
|
||||
|
||||
# Reinit graph client
|
||||
try:
|
||||
scheduler.graph = GraphAPI()
|
||||
logger.info("Graph-Client nach Web-Auth neu initialisiert")
|
||||
except Exception as e:
|
||||
logger.error("Graph-Client Reinit fehlgeschlagen: %s", e)
|
||||
|
||||
except Exception as e:
|
||||
_auth_state["status"] = "error"
|
||||
_auth_state["error"] = str(e)
|
||||
logger.error("Web-Auth fehlgeschlagen: %s", e)
|
||||
|
||||
|
||||
@app.post("/api/azure/auth")
|
||||
async def start_azure_auth():
|
||||
"""Start device code flow in background."""
|
||||
import threading
|
||||
_auth_state["status"] = "starting"
|
||||
_auth_state["user_code"] = None
|
||||
_auth_state["verification_uri"] = None
|
||||
_auth_state["error"] = None
|
||||
|
||||
# Clear old auth record
|
||||
database.set_setting("azure_auth_record", "")
|
||||
|
||||
thread = threading.Thread(target=_run_device_code_auth, daemon=True)
|
||||
thread.start()
|
||||
return JSONResponse({"status": "starting"})
|
||||
|
||||
|
||||
@app.get("/api/azure/auth/status")
|
||||
async def azure_auth_status():
|
||||
"""Poll auth state from frontend."""
|
||||
return JSONResponse(_auth_state)
|
||||
|
||||
|
||||
# ── CLI Auth Command ───────────────────────────────────────────────────────
|
||||
|
||||
def cli_auth():
|
||||
|
||||
Reference in New Issue
Block a user