feat: Kommando zum Setzen des Passworts
CI / backend (push) Successful in 2m49s
Images bauen / build (frontend) (push) Successful in 3m12s
Images bauen / build (backend) (push) Successful in 3m22s
CI / frontend (push) Successful in 6m20s

Die Erstanlage beim Start greift nur, solange die Benutzertabelle leer ist. Wer
.env.example zuerst unverändert übernimmt, bekommt einen Benutzer mit dem
Platzhalter; ein späteres Ändern von MONEYFY_ADMIN_PASSWORD bleibt danach
wirkungslos. Das Kommando setzt das Passwort direkt, legt den Benutzer bei
Bedarf an und beendet alle offenen Sitzungen.

docs/deployment.md nennt jetzt die drei Ursachen einer fehlgeschlagenen
Anmeldung in der Reihenfolge ihrer Häufigkeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
Jonas Menzel
2026-09-09 23:40:42 +02:00
co-authored by Claude Opus 5
parent a022a99f97
commit c110340628
5 changed files with 157 additions and 6 deletions
+108
View File
@@ -0,0 +1,108 @@
"""CLI: Passwort setzen oder Administrator anlegen.
python -m app.scripts.reset_password
python -m app.scripts.reset_password --username admin --password geheim123456
Nötig, weil die Erstanlage beim Start nur greift, solange noch kein Benutzer
existiert ein nachträglich geändertes `MONEYFY_ADMIN_PASSWORD` bleibt danach
wirkungslos.
"""
import argparse
import asyncio
import getpass
import os
import sys
from sqlalchemy import select
from app.core.config import settings
from app.core.security import hash_password
from app.db.session import SessionLocal, engine
from app.models import AppUser
from app.services.auth import (
MIN_PASSWORD_LENGTH,
get_user_by_username,
revoke_all_refresh_tokens,
)
def _passwort_ermitteln(vorgabe: str | None) -> str:
"""Reihenfolge: Argument, Umgebungsvariable, interaktive Eingabe."""
if vorgabe:
return vorgabe
aus_umgebung = os.environ.get("MONEYFY_ADMIN_PASSWORD")
if aus_umgebung:
print("Verwende MONEYFY_ADMIN_PASSWORD aus der Umgebung.")
return aus_umgebung
if not sys.stdin.isatty():
raise SystemExit(
"Kein Passwort angegeben. Entweder --password nutzen oder "
"MONEYFY_ADMIN_PASSWORD setzen."
)
erste = getpass.getpass("Neues Passwort: ")
zweite = getpass.getpass("Wiederholen: ")
if erste != zweite:
raise SystemExit("Die Eingaben stimmen nicht überein.")
return erste
async def main(username: str, passwort: str, force_change: bool) -> int:
if len(passwort) < MIN_PASSWORD_LENGTH:
print(
f"Das Passwort muss mindestens {MIN_PASSWORD_LENGTH} Zeichen lang sein.",
file=sys.stderr,
)
return 1
async with SessionLocal() as session:
benutzer = await get_user_by_username(session, username)
if benutzer is None:
vorhandene = (await session.execute(select(AppUser.username))).scalars().all()
benutzer = AppUser(username=username, must_change_password=force_change)
session.add(benutzer)
aktion = "angelegt"
if vorhandene:
print(f"Vorhandene Benutzer: {', '.join(vorhandene)}")
else:
aktion = "geändert"
benutzer.must_change_password = force_change
# Ein Passwortwechsel beendet alle offenen Sitzungen.
await revoke_all_refresh_tokens(session, benutzer.id)
benutzer.password_hash = hash_password(passwort)
await session.commit()
await engine.dispose()
print(f"Passwort für '{username}' {aktion}.")
if force_change:
print("Beim ersten Anmelden wird ein neues Passwort verlangt.")
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--username",
default=settings.moneyfy_admin_user,
help=f"Benutzername (Vorgabe: {settings.moneyfy_admin_user}).",
)
parser.add_argument(
"--password",
help="Neues Passwort. Ohne Angabe wird MONEYFY_ADMIN_PASSWORD genutzt "
"oder interaktiv gefragt.",
)
parser.add_argument(
"--force-change",
action="store_true",
help="Verlangt beim nächsten Anmelden erneut einen Passwortwechsel.",
)
args = parser.parse_args()
raise SystemExit(
asyncio.run(main(args.username, _passwort_ermitteln(args.password), args.force_change))
)