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:
2026-05-03 10:03:04 +02:00
co-authored by Claude Opus 4.6
parent d5d5160257
commit bae07e78e7
2 changed files with 161 additions and 8 deletions
+75 -1
View File
@@ -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():