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
109 lines
3.4 KiB
Python
109 lines
3.4 KiB
Python
"""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))
|
||
)
|