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
This commit is contained in:
co-authored by
Claude Opus 5
parent
a022a99f97
commit
c110340628
@@ -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))
|
||||
)
|
||||
Reference in New Issue
Block a user