Mac-Worker: - /api/summarize, /api/protocol, /api/preload akzeptieren optionales model-Feld → überschreibt den .env-Default pro Call, kein Restart nötig - /api/models listet die lokal verfügbaren Ollama-Modelle + Default LXC: - User.default_model + Job.model (Migration für SQLite) - /api/auth/me liefert default_model, PATCH speichert ihn sofort - /api/mac/models proxiet die Liste an die UI - Pipeline reicht job.model an Mac-Worker durch; leer = Worker-Default - create_job & reprocess akzeptieren model-Override - Snapshot-Meta nutzt job.model (genauer als /health-Snapshot) UI: - Header-Dropdown "Modell:" neben "Profil:" — sofort-speichert User-Default - Jeder Job zeigt sein Modell als Tag (lila); leer bedeutet Worker-Default - Reprocess öffnet einen Modal-Picker (statt simplem confirm), Auswahl aller verfügbaren Modelle inkl. "Worker-Default" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
"""Auth-Endpoints: Login, Logout, Me, Erst-Setup (Initial-Admin)."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlmodel import Session
|
|
|
|
from app.core import migrate
|
|
from app.core.auth import (
|
|
current_user,
|
|
get_user_by_username,
|
|
has_any_user,
|
|
hash_password,
|
|
verify_password,
|
|
)
|
|
from app.core.db import get_session
|
|
from app.models.user import User
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
log = logging.getLogger("voice-agent.auth")
|
|
|
|
|
|
class LoginIn(BaseModel):
|
|
username: str = Field(min_length=1, max_length=64)
|
|
password: str = Field(min_length=1, max_length=200)
|
|
|
|
|
|
class SetupIn(BaseModel):
|
|
username: str = Field(min_length=3, max_length=64, pattern=r"^[A-Za-z0-9._@-]+$")
|
|
password: str = Field(min_length=8, max_length=200)
|
|
|
|
|
|
def _user_dict(u: User) -> dict:
|
|
return {
|
|
"id": u.id,
|
|
"username": u.username,
|
|
"is_admin": u.is_admin,
|
|
"default_profile": u.default_profile or "meeting",
|
|
# Leerer String = User hat keine Modell-Wahl getroffen → Mac-Worker
|
|
# nimmt seinen .env-Default. UI zeigt das als „Standard (Worker)".
|
|
"default_model": u.default_model or "",
|
|
}
|
|
|
|
|
|
class UpdateMeIn(BaseModel):
|
|
default_profile: str | None = Field(default=None, max_length=64)
|
|
# Leerer String bedeutet bewusst „kein Override" — wird gespeichert.
|
|
default_model: str | None = Field(default=None, max_length=128)
|
|
|
|
|
|
@router.get("/me")
|
|
def me(user: User = Depends(current_user)):
|
|
return _user_dict(user)
|
|
|
|
|
|
@router.patch("/me")
|
|
def update_me(payload: UpdateMeIn, user: User = Depends(current_user), session: Session = Depends(get_session)):
|
|
changed = False
|
|
if payload.default_profile is not None:
|
|
user.default_profile = payload.default_profile.strip() or "meeting"
|
|
changed = True
|
|
if payload.default_model is not None:
|
|
user.default_model = payload.default_model.strip()
|
|
changed = True
|
|
if changed:
|
|
session.add(user)
|
|
session.commit()
|
|
session.refresh(user)
|
|
return _user_dict(user)
|
|
|
|
|
|
@router.get("/needs-setup")
|
|
def needs_setup(session: Session = Depends(get_session)):
|
|
"""Vor Login prüfen ob noch kein User existiert (erstes Deployment)."""
|
|
return {"needs_setup": not has_any_user(session)}
|
|
|
|
|
|
@router.post("/login")
|
|
def login(payload: LoginIn, request: Request, session: Session = Depends(get_session)):
|
|
username = payload.username.strip()
|
|
user = get_user_by_username(session, username)
|
|
if not user:
|
|
log.warning("Login fehlgeschlagen: User '%s' nicht gefunden", username)
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Benutzername oder Passwort falsch")
|
|
if not verify_password(payload.password, user.password_hash):
|
|
log.warning(
|
|
"Login fehlgeschlagen: Passwort falsch für user=%s (hash_len=%d, prefix=%s)",
|
|
user.username, len(user.password_hash), user.password_hash[:7],
|
|
)
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Benutzername oder Passwort falsch")
|
|
request.session["user_id"] = user.id
|
|
log.info("Login OK: user=%s id=%d admin=%s", user.username, user.id, user.is_admin)
|
|
return _user_dict(user)
|
|
|
|
|
|
@router.post("/logout")
|
|
def logout(request: Request):
|
|
request.session.clear()
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/setup")
|
|
def setup(payload: SetupIn, request: Request, session: Session = Depends(get_session)):
|
|
"""Erstellt den ersten Admin und ordnet alle Bestand-Jobs ihm zu.
|
|
Nur möglich, solange kein User existiert."""
|
|
if has_any_user(session):
|
|
raise HTTPException(status.HTTP_409_CONFLICT, "Setup bereits abgeschlossen")
|
|
user = User(
|
|
username=payload.username.strip(),
|
|
password_hash=hash_password(payload.password),
|
|
is_admin=True,
|
|
)
|
|
session.add(user)
|
|
session.commit()
|
|
session.refresh(user)
|
|
log.info("Initial-Admin angelegt: %s (id=%d)", user.username, user.id)
|
|
n = migrate.assign_orphan_jobs_to(user.id)
|
|
request.session["user_id"] = user.id
|
|
return {**_user_dict(user), "migrated_jobs": n}
|