Files
moneyfy/backend/app/scripts/reset_password.py
T
Jonas MenzelandClaude Opus 5 c110340628
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
feat: Kommando zum Setzen des Passworts
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
2026-09-09 23:40:42 +02:00

109 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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))
)