Aendert der Anmeldedienst den Namen eines Nutzers, muss Certain folgen
koennen. Bisher ging das gar nicht: Umbenennen war nirgends vorgesehen.
Die gute Nachricht vorweg – am Fundament war nichts zu richten. Buchungen,
Konten, Vertraege, Sitzungen und Geraeteschluessel haengen alle an der NUMMER
des Nutzers; der Name ist nirgends ein Fremdschluessel, und auch das
Wiedereinspielen einer Sicherung ordnet nicht ueber ihn zu. Umbenennen zieht
also nichts mit. Ein Test haelt das jetzt fest: Nach dem Umbenennen sind es
dieselbe Nummer, dasselbe Konto und derselbe Saldo.
Zwei Wege gibt es jetzt.
Der Admin kann umbenennen (PATCH /api/users/{id}). Eindeutig bleiben muss der
Name trotzdem – er ist das, was beim Passwort-Login eingetippt wird.
Und der Anmeldedienst zieht bei jeder Anmeldung nach. Dabei zaehlt, WESSEN
Name das jeweils ist: Der Anzeigename gehoert dem Anbieter – heiratet jemand,
soll der neue Name auch hier stehen. Der Benutzername gehoert bei einem Konto
MIT Passwort dem Nutzer; ihn hinter dessen Ruecken zu aendern hiesse, ihm
seine Anmeldung wegzunehmen. Nur bei einem reinen SSO-Konto gibt es nichts
einzutippen – dort darf der Anbieter bestimmen.
Ist der neue Name schon vergeben, bleibt der alte stehen. Eine Anmeldung
daran scheitern zu lassen waere die schlechtere Antwort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.8 KiB
Python
123 lines
4.8 KiB
Python
"""Benutzerverwaltung (nur Admin). Neue Nutzer bekommen ein Temporär-Passwort."""
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from sqlalchemy.orm import Session
|
||
|
||
from ..database import get_db
|
||
from ..deps import require_admin, user_out
|
||
from ..models import Role, User
|
||
from ..schemas import UserCreate, UserOut, UserUpdate
|
||
from ..security import hash_password
|
||
from ..seed import ensure_starter_categories
|
||
from ..services import policy as policy_service
|
||
from ..services import sessions, twofactor
|
||
|
||
router = APIRouter(prefix="/users", tags=["users"], dependencies=[Depends(require_admin)])
|
||
|
||
|
||
@router.get("", response_model=list[UserOut])
|
||
def list_users(db: Session = Depends(get_db)) -> list[UserOut]:
|
||
rows = db.query(User).order_by(User.username).all()
|
||
return [user_out(db, u) for u in rows]
|
||
|
||
|
||
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
||
def create_user(payload: UserCreate, db: Session = Depends(get_db)) -> UserOut:
|
||
if db.query(User).filter(User.username == payload.username).first():
|
||
raise HTTPException(status.HTTP_409_CONFLICT, "Benutzername existiert bereits")
|
||
policy_service.assert_ok(db, payload.password)
|
||
user = User(
|
||
username=payload.username,
|
||
password_hash=hash_password(payload.password),
|
||
role=payload.role,
|
||
display_name=payload.display_name,
|
||
is_active=True,
|
||
# Vom Admin gesetztes Temporär-Passwort: beim ersten Login ändern lassen.
|
||
must_change_password=True,
|
||
must_change_password_reason="temp",
|
||
)
|
||
db.add(user)
|
||
db.commit()
|
||
db.refresh(user)
|
||
# Neuen Nutzern die Startkategorien mitgeben.
|
||
ensure_starter_categories(db, user)
|
||
return user_out(db, user)
|
||
|
||
|
||
@router.patch("/{user_id}", response_model=UserOut)
|
||
def update_user(user_id: int, payload: UserUpdate, db: Session = Depends(get_db)) -> UserOut:
|
||
user = db.get(User, user_id)
|
||
if user is None:
|
||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Benutzer nicht gefunden")
|
||
if payload.username is not None:
|
||
neu = payload.username.strip()
|
||
if not neu:
|
||
raise HTTPException(status.HTTP_400_BAD_REQUEST,
|
||
"Benutzername darf nicht leer sein")
|
||
# Der Name ist ein Etikett, kein Schluessel – umbenennen zieht nichts
|
||
# mit. Eindeutig bleiben muss er trotzdem: Er ist das, was beim
|
||
# Passwort-Login eingetippt wird.
|
||
belegt = (
|
||
db.query(User.id)
|
||
.filter(User.username == neu, User.id != user.id)
|
||
.first()
|
||
)
|
||
if belegt is not None:
|
||
raise HTTPException(status.HTTP_409_CONFLICT,
|
||
"Benutzername existiert bereits")
|
||
user.username = neu
|
||
if payload.password is not None:
|
||
policy_service.assert_ok(db, payload.password)
|
||
user.password_hash = hash_password(payload.password)
|
||
# Vom Admin gesetztes Passwort gilt als temporär.
|
||
user.must_change_password = True
|
||
user.must_change_password_reason = "temp"
|
||
# Ein zurückgesetztes Passwort soll bestehende Sitzungen beenden.
|
||
sessions.revoke_all(db, user)
|
||
if payload.role is not None:
|
||
user.role = payload.role
|
||
if payload.display_name is not None:
|
||
user.display_name = payload.display_name
|
||
if payload.is_active is not None:
|
||
user.is_active = payload.is_active
|
||
db.commit()
|
||
db.refresh(user)
|
||
return user_out(db, user)
|
||
|
||
|
||
@router.post("/{user_id}/2fa/reset", response_model=UserOut)
|
||
def reset_two_factor(user_id: int, db: Session = Depends(get_db)) -> UserOut:
|
||
"""Notausgang, wenn jemand seinen Authenticator und alle Codes verloren hat.
|
||
|
||
Verlangt die Richtlinie MFA, muss der Benutzer beim nächsten Anmelden sofort
|
||
neu einrichten – dafür sorgt ``enforce_account_state``."""
|
||
user = db.get(User, user_id)
|
||
if user is None:
|
||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Benutzer nicht gefunden")
|
||
twofactor.disable(db, user)
|
||
sessions.revoke_all(db, user)
|
||
db.refresh(user)
|
||
return user_out(db, user)
|
||
|
||
|
||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
def delete_user(
|
||
user_id: int,
|
||
db: Session = Depends(get_db),
|
||
current_admin: User = Depends(require_admin),
|
||
) -> None:
|
||
user = db.get(User, user_id)
|
||
if user is None:
|
||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Benutzer nicht gefunden")
|
||
if user.id == current_admin.id:
|
||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Du kannst dich nicht selbst löschen")
|
||
if user.role == Role.admin:
|
||
admin_count = db.query(User).filter(User.role == Role.admin).count()
|
||
if admin_count <= 1:
|
||
raise HTTPException(
|
||
status.HTTP_400_BAD_REQUEST,
|
||
"Der letzte Administrator kann nicht gelöscht werden",
|
||
)
|
||
db.delete(user)
|
||
db.commit()
|