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:
parent
d5d5160257
commit
bae07e78e7
76
app.py
76
app.py
@ -11,7 +11,7 @@ import sys
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime
|
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.responses import HTMLResponse, RedirectResponse, FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
@ -859,6 +859,80 @@ async def delete_firmware(filename: str):
|
|||||||
return RedirectResponse("/devices", status_code=303)
|
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 ───────────────────────────────────────────────────────
|
# ── CLI Auth Command ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def cli_auth():
|
def cli_auth():
|
||||||
|
|||||||
@ -43,19 +43,53 @@
|
|||||||
placeholder="User.Read User.Read.All Presence.Read Presence.Read.All">
|
placeholder="User.Read User.Read.All Presence.Read Presence.Read.All">
|
||||||
<small class="text-muted">Standard: User.Read User.Read.All Presence.Read Presence.Read.All</small>
|
<small class="text-muted">Standard: User.Read User.Read.All Presence.Read Presence.Read.All</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="border rounded p-3 bg-light">
|
<div id="auth-status" class="border rounded p-3 bg-light">
|
||||||
{% if authenticated %}
|
{% if authenticated %}
|
||||||
<i class="bi bi-check-circle text-success"></i>
|
<i class="bi bi-check-circle text-success"></i>
|
||||||
<strong class="text-success">Authentifiziert</strong>
|
<strong class="text-success">Verbunden</strong>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-warning" onclick="startAuth()">
|
||||||
|
<i class="bi bi-arrow-repeat"></i> Neu verbinden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-x-circle text-danger"></i>
|
<i class="bi bi-x-circle text-danger"></i>
|
||||||
<strong class="text-danger">Nicht authentifiziert</strong>
|
<strong class="text-danger">Nicht verbunden</strong>
|
||||||
<p class="small text-muted mt-2 mb-0">
|
<div class="mt-2">
|
||||||
Fuehre auf dem Server aus:<br>
|
<button type="button" class="btn btn-sm btn-primary" onclick="startAuth()">
|
||||||
<code>python app.py --auth</code>
|
<i class="bi bi-microsoft"></i> Mit Azure verbinden
|
||||||
</p>
|
</button>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="auth-flow" class="border rounded p-3 bg-light mt-2" style="display:none;">
|
||||||
|
<div id="auth-waiting" style="display:none;">
|
||||||
|
<p class="mb-2"><strong>Schritt 1:</strong> Oeffne den folgenden Link:</p>
|
||||||
|
<p><a id="auth-url" href="#" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-box-arrow-up-right"></i> Bei Microsoft anmelden
|
||||||
|
</a></p>
|
||||||
|
<p class="mb-2"><strong>Schritt 2:</strong> Gib diesen Code ein:</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<span id="auth-code" class="badge bg-dark fs-4 font-monospace p-3"></span>
|
||||||
|
</p>
|
||||||
|
<p class="small text-muted mb-0">
|
||||||
|
<i class="bi bi-hourglass-split"></i> Warte auf Anmeldung...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="auth-starting" style="display:none;">
|
||||||
|
<i class="bi bi-hourglass-split"></i> Verbindung wird hergestellt...
|
||||||
|
</div>
|
||||||
|
<div id="auth-success" style="display:none;">
|
||||||
|
<i class="bi bi-check-circle text-success"></i>
|
||||||
|
<strong class="text-success">Erfolgreich verbunden!</strong>
|
||||||
|
<p class="small text-muted mt-1 mb-0">Seite wird neu geladen...</p>
|
||||||
|
</div>
|
||||||
|
<div id="auth-error" style="display:none;">
|
||||||
|
<i class="bi bi-exclamation-triangle text-danger"></i>
|
||||||
|
<strong class="text-danger">Fehler:</strong>
|
||||||
|
<span id="auth-error-msg"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -132,3 +166,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let authPollInterval = null;
|
||||||
|
|
||||||
|
function startAuth() {
|
||||||
|
document.getElementById('auth-flow').style.display = 'block';
|
||||||
|
document.getElementById('auth-starting').style.display = 'block';
|
||||||
|
document.getElementById('auth-waiting').style.display = 'none';
|
||||||
|
document.getElementById('auth-success').style.display = 'none';
|
||||||
|
document.getElementById('auth-error').style.display = 'none';
|
||||||
|
|
||||||
|
fetch('/api/azure/auth', {method: 'POST'})
|
||||||
|
.then(() => {
|
||||||
|
authPollInterval = setInterval(pollAuthStatus, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollAuthStatus() {
|
||||||
|
fetch('/api/azure/auth/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'waiting') {
|
||||||
|
document.getElementById('auth-starting').style.display = 'none';
|
||||||
|
document.getElementById('auth-waiting').style.display = 'block';
|
||||||
|
document.getElementById('auth-code').textContent = data.user_code;
|
||||||
|
const url = document.getElementById('auth-url');
|
||||||
|
url.href = data.verification_uri;
|
||||||
|
} else if (data.status === 'success') {
|
||||||
|
clearInterval(authPollInterval);
|
||||||
|
document.getElementById('auth-waiting').style.display = 'none';
|
||||||
|
document.getElementById('auth-starting').style.display = 'none';
|
||||||
|
document.getElementById('auth-success').style.display = 'block';
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
clearInterval(authPollInterval);
|
||||||
|
document.getElementById('auth-waiting').style.display = 'none';
|
||||||
|
document.getElementById('auth-starting').style.display = 'none';
|
||||||
|
document.getElementById('auth-error').style.display = 'block';
|
||||||
|
document.getElementById('auth-error-msg').textContent = data.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user