root a1167df4f1 feat: User-Verwaltung mit Login, Sessions und Owner-Filter
Backend:
- User-Modell (sqlmodel) mit bcrypt-Passwort-Hash und is_admin-Flag
- Job.owner_id (FK auf user.id), per Migration nachgerüstet (SQLite ALTER)
- Starlette SessionMiddleware mit signiertem Cookie (SESSION_SECRET)
- /api/auth: login / logout / me / needs-setup / setup (Initial-Admin)
- /api/admin: User-CRUD + Passwort-Reset (Admin-only); letzter Admin /
  eigener Account vor Löschung geschützt
- /api/jobs: Owner-Filter (Admin sieht alle inkl. owner_username),
  404 statt 403 bei fremden Jobs (kein Existenz-Leak)

Frontend:
- Auth-Overlay mit Login bzw. Erst-Setup-Modus (auto-Detection)
- Header zeigt User + Logout, Admin-Badge bei Admin-Konten
- Admin-Karte: User anlegen, Passwort zurücksetzen, Löschen (mit Confirm)
- 401 → Login-Overlay (auch bei XHR-Upload)

Deploy:
- install.sh generiert SESSION_SECRET einmalig per python-secrets
- .env wird auf chmod 600 gesetzt
- Erstaufruf zeigt automatisch das Setup-Formular; bestehende Jobs werden
  dem ersten Admin zugeordnet
2026-05-14 02:38:52 +00:00

82 lines
2.6 KiB
Python

import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from starlette.types import Scope
from app.api.admin import router as admin_router
from app.api.auth import router as auth_router
from app.api.jobs import router as jobs_router
from app.core.config import settings
from app.core.db import init_db
from app.core.mac_client import MacClient
from app.core.migrate import run_migrations
class NoCacheStaticFiles(StaticFiles):
"""StaticFiles, die Browser zu Revalidierung zwingen (verhindert hängende Caches nach Deploy)."""
async def get_response(self, path: str, scope: Scope):
response = await super().get_response(path, scope)
response.headers["Cache-Control"] = "no-cache, must-revalidate"
return response
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
log = logging.getLogger("voice-agent")
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
run_migrations()
if settings.session_secret == "CHANGE-ME-SET-IN-ENV":
log.warning("SESSION_SECRET nicht gesetzt — bitte in .env auf einen geheimen Wert ändern!")
log.info("DB initialized at %s", settings.db_url)
log.info("Upload dir: %s", settings.upload_dir)
log.info("Result dir: %s", settings.result_dir)
log.info("Mac API: %s", settings.mac_api_url)
yield
app = FastAPI(title=settings.app_name, lifespan=lifespan)
app.add_middleware(
SessionMiddleware,
secret_key=settings.session_secret,
session_cookie=settings.session_cookie_name,
max_age=settings.session_max_age,
same_site="lax",
https_only=False, # via Reverse-Proxy: ggf. auf True wenn HTTPS terminiert
)
app.include_router(auth_router)
app.include_router(admin_router)
app.include_router(jobs_router)
STATIC_DIR = Path(__file__).resolve().parent / "static"
app.mount("/static", NoCacheStaticFiles(directory=STATIC_DIR), name="static")
@app.get("/")
def index():
return FileResponse(STATIC_DIR / "index.html", headers={"Cache-Control": "no-cache, must-revalidate"})
@app.get("/health")
def health():
return {"status": "ok", "service": settings.app_name}
@app.get("/api/mac/health")
async def mac_health():
try:
data = await MacClient().health()
return {"reachable": True, "mac": data}
except Exception as e: # noqa: BLE001
return JSONResponse(status_code=502, content={"reachable": False, "error": str(e)})