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
+6 -1
View File
@@ -7,7 +7,12 @@ die Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
## [Unreleased]
Noch keine Änderungen seit 0.1.1.
### Hinzugefügt
- `python -m app.scripts.reset_password` setzt das Passwort eines Benutzers oder
legt ihn an. Nötig, weil die Erstanlage beim Start nur bei leerer
Benutzertabelle greift und ein nachträglich geändertes
`MONEYFY_ADMIN_PASSWORD` danach wirkungslos bleibt.
## [0.1.1] 2026-09-09
+8
View File
@@ -32,10 +32,18 @@ seed: ## Stammdaten (Kategoriebaum, Benachrichtigungsregeln) anlegen
vendor-icons: ## simple-icons-Index neu erzeugen (benötigt npm und Netzzugang)
cd $(BACKEND) && .venv/bin/python scripts/vendor_simple_icons.py
.PHONY: reset-password
reset-password: ## Passwort setzen oder Administrator anlegen: make reset-password u=admin
cd $(BACKEND) && .venv/bin/python -m app.scripts.reset_password $(if $(u),--username $(u),)
.PHONY: seed-demo
seed-demo: ## Realistische Demodaten anlegen (vorhandene Daten bleiben, sonst --reset)
cd $(BACKEND) && .venv/bin/python -m app.scripts.seed_demo
.PHONY: reset-password
reset-password: ## Passwort setzen oder Administrator anlegen: make reset-password u=admin
cd $(BACKEND) && .venv/bin/python -m app.scripts.reset_password $(if $(u),--username $(u),)
.PHONY: seed-demo-reset
seed-demo-reset: ## Demodaten neu aufsetzen; löscht Konten, Firmen, Posten und Buchungen
cd $(BACKEND) && .venv/bin/python -m app.scripts.seed_demo --reset
+1 -1
View File
@@ -134,7 +134,7 @@ vollständige, kommentierte Liste. Die wichtigsten:
|---|---|---|
| `DATABASE_URL` | | PostgreSQL-DSN, `postgresql://` wird auf asyncpg umgestellt |
| `SECRET_KEY` | | Signaturschlüssel für JWTs, mind. 32 Zeichen (`openssl rand -hex 32`) |
| `MONEYFY_ADMIN_USER` / `MONEYFY_ADMIN_PASSWORD` | `admin` / | Beim Erststart angelegter Benutzer |
| `MONEYFY_ADMIN_USER` / `MONEYFY_ADMIN_PASSWORD` | `admin` / | Beim Erststart angelegter Benutzer. Es gibt **kein** eingebautes Standardpasswort; ohne diesen Wert wird niemand angelegt. Die Anlage greift nur, solange die Benutzertabelle leer ist danach hilft `app.scripts.reset_password`. |
| `TIMEZONE` | `Europe/Berlin` | Zeitzone der gesamten Anwendung |
| `HOLIDAY_REGION` | `DE-NW` | Feiertagsregion für Werktagsverschiebungen |
| `LOGO_STORAGE_DIR` | `/data/logos` | Verzeichnis des Logo-Caches |
+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))
)
+34 -4
View File
@@ -188,10 +188,40 @@ Die Anwendung verweigert in Produktion den Start mit dem Platzhalterwert.
`openssl rand -hex 32` erzeugt einen passenden Schlüssel. Wird er später
geändert, sind alle Sitzungen ungültig das ist beabsichtigt.
**Anmeldung schlägt fehl, obwohl das Passwort stimmt**
Meist fehlt HTTPS: Die Cookies sind `Secure` gesetzt und erreichen den Server
über eine reine HTTP-Verbindung nicht. Entweder über NPMplus zugreifen oder
`COOKIE_SECURE=false` setzen.
**Anmeldung schlägt fehl**
Drei Ursachen kommen in Frage, in dieser Reihenfolge prüfen:
1. **Der Benutzer wurde mit einem anderen Passwort angelegt.** Die Erstanlage
greift nur, solange die Benutzertabelle leer ist. Wer `.env.example` zuerst
unverändert übernimmt und startet, bekommt einen Benutzer mit dem Platzhalter
ein späteres Ändern von `MONEYFY_ADMIN_PASSWORD` bleibt dann wirkungslos.
Abhilfe:
```bash
docker compose exec moneyfy-backend python -m app.scripts.reset_password \
--username admin --password "neues-passwort"
```
2. **Der Container kennt den neuen Wert noch nicht.** `docker compose restart`
behält die alte Umgebung. Nach einer Änderung an der `.env` muss der
Container neu erstellt werden:
```bash
docker compose up -d --force-recreate moneyfy-backend
docker compose exec moneyfy-backend printenv MONEYFY_ADMIN_PASSWORD
```
3. **Kein HTTPS.** Die Cookies sind `Secure` gesetzt und erreichen den Server
über eine reine HTTP-Verbindung nicht. Typisches Bild: die Anmeldung meldet
keinen Fehler, landet aber sofort wieder auf der Anmeldemaske. Entweder über
NPMplus zugreifen oder für einen Test `COOKIE_SECURE=false` setzen.
Welcher Fall vorliegt, verrät das Log:
```bash
docker compose logs moneyfy-backend | grep -iE "administrator|MONEYFY_ADMIN"
```
**Firmen bekommen kein Logo**
Ohne Netzzugang findet nur der lokale Markenkatalog etwas; unbekannte Firmen