feat: Cloud-Fundament - Auth, Persistenz und Mandanten-Trennung

- SQLite-Store (app/store.py) hinter Store-Interface: Nutzer + Sessions persistent
- Bearer-Token-Auth (app/auth.py); Nutzerverwaltung via Admin-Key (POST /api/admin/users)
- GET /api/me, PUT /api/me/prefs (dauerhafte Nutzer-Praeferenzen)
- chat/speak/transcribe/sessions auth-geschuetzt; Mandanten-Trennung (fremde Session -> 403)
- Route-Aufloesung: Defaults < Profil < ENV < Nutzer-Prefs < Session < Request
- SessionManager (in-memory) durch Store ersetzt
- AUTH_ENABLED-Schalter (prod an, dev/Tests aus); DB_PATH/ADMIN_API_KEY
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur, deploy-env); data/ gitignored
- Tests: 29 gruen (Auth, Mandanten, Persistenz, Routing)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 02:14:25 +02:00
commit e0e69fdf15
22 changed files with 625 additions and 57 deletions

14
app/api/admin.py Normal file
View file

@ -0,0 +1,14 @@
from fastapi import APIRouter, Depends
from app.auth import require_admin
from app.dependencies import get_store
from app.schemas import UserCreate, UserCreated
router = APIRouter()
@router.post("/admin/users", response_model=UserCreated, dependencies=[Depends(require_admin)])
async def create_user(payload: UserCreate):
"""Legt einen Nutzer an und gibt das Bearer-Token EINMALIG zurueck."""
user, token = get_store().create_user(payload.display_name)
return UserCreated(user_id=user.id, display_name=user.display_name, token=token)

View file

@ -1,10 +1,12 @@
from io import BytesIO
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import JSONResponse, StreamingResponse
from app.config import settings
from app.errors import RoutingError
from app.auth import require_user
from app.store import User, SessionOwnershipError
from app.dependencies import (
resolve_route,
build_orchestrator,
@ -36,6 +38,7 @@ async def chat(
default=None,
description="Optional session id to apply a stored route",
),
user: User = Depends(require_user),
):
overrides = {
"input_endpoint": payload.input_endpoint,
@ -45,12 +48,14 @@ async def chat(
"llm_provider": payload.llm_provider,
"tts_provider": payload.tts_provider,
}
route = resolve_route(session_id, overrides)
voice = payload.voice or settings.openrouter_tts_voice
try:
route = resolve_route(user, session_id, overrides)
orchestrator = build_orchestrator(route)
output = await resolve_output_endpoint(route)
except SessionOwnershipError as exc:
raise HTTPException(status_code=403, detail=str(exc))
except RoutingError as exc:
raise HTTPException(status_code=422, detail=str(exc))

21
app/api/me.py Normal file
View file

@ -0,0 +1,21 @@
from fastapi import APIRouter, Depends
from app.auth import require_user
from app.dependencies import get_store
from app.schemas import UserPrefs
from app.store import User
router = APIRouter()
@router.get("/me")
async def get_me(user: User = Depends(require_user)):
return {"user_id": user.id, "display_name": user.display_name, "prefs": user.prefs}
@router.put("/me/prefs")
async def set_my_prefs(payload: UserPrefs, user: User = Depends(require_user)):
"""Setzt die dauerhaften Routing-Praeferenzen des Nutzers (nur gesetzte Felder)."""
prefs = {k: v for k, v in payload.model_dump().items() if v is not None}
updated = get_store().set_user_prefs(user.id, prefs)
return {"user_id": updated.id, "prefs": updated.prefs}

View file

@ -1,10 +1,21 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends, HTTPException
from app.schemas import SessionRouteRequest
from app.dependencies import session_manager
from app.auth import require_user
from app.dependencies import get_store
from app.store import User, SessionOwnershipError
router = APIRouter()
@router.post("/sessions/{session_id}/route")
async def set_session_route(session_id: str, payload: SessionRouteRequest):
session = session_manager.update(session_id, payload.model_dump())
return {"session_id": session_id, "route": session}
async def set_session_route(
session_id: str,
payload: SessionRouteRequest,
user: User = Depends(require_user),
):
try:
session = get_store().update_session(session_id, user.id, payload.model_dump())
except SessionOwnershipError as exc:
raise HTTPException(status_code=403, detail=str(exc))
return {"session_id": session_id, "route": session.data}

View file

@ -1,10 +1,12 @@
from io import BytesIO
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from app.config import settings
from app.errors import RoutingError
from app.auth import require_user
from app.store import User, SessionOwnershipError
from app.dependencies import (
resolve_route,
build_orchestrator,
@ -22,18 +24,21 @@ async def speak(
default=None,
description="Optional session id to apply a stored route",
),
user: User = Depends(require_user),
):
overrides = {
"output_endpoint": payload.output_endpoint,
"language": payload.language,
"tts_provider": payload.tts_provider,
}
route = resolve_route(session_id, overrides)
voice = payload.voice or settings.openrouter_tts_voice
try:
route = resolve_route(user, session_id, overrides)
orchestrator = build_orchestrator(route)
output = await resolve_output_endpoint(route)
except SessionOwnershipError as exc:
raise HTTPException(status_code=403, detail=str(exc))
except RoutingError as exc:
raise HTTPException(status_code=422, detail=str(exc))

View file

@ -1,6 +1,8 @@
from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from app.errors import RoutingError
from app.auth import require_user
from app.store import User, SessionOwnershipError
from app.dependencies import (
resolve_route,
build_orchestrator,
@ -20,17 +22,20 @@ async def transcribe(
default=None,
description="Optional session id to apply a stored route",
),
user: User = Depends(require_user),
):
overrides = {
"input_endpoint": input_endpoint,
"language": language,
"stt_provider": stt_provider,
}
route = resolve_route(session_id, overrides)
try:
route = resolve_route(user, session_id, overrides)
orchestrator = build_orchestrator(route)
source = await resolve_input_endpoint(route)
except SessionOwnershipError as exc:
raise HTTPException(status_code=403, detail=str(exc))
except RoutingError as exc:
raise HTTPException(status_code=422, detail=str(exc))