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
+6
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user