feat: Phase 0 – Gerüst (FastAPI+SQLite, Multi-User-Login, React-App-Shell, Deploy)

Grundgerüst für Certain Finance:
- Backend: FastAPI + SQLAlchemy + SQLite, Alembic-Migrationen (Batch-Mode),
  argon2-Passwort-Hashing, server-seitige Sitzungen im Cookie, Erst-Admin,
  Benutzerverwaltung mit Temporär-Passwort, Einstellungen je Benutzer.
- Ein Prozess, ein Port: uvicorn liefert API (/api) und gebautes SPA aus,
  inkl. SPA-Deeplink-Fallback.
- Frontend: React 18 + Vite + JSX, Design-Tokens (Light/Dark), i18n DE/EN,
  Inline-SVG-Icons (keine Emojis), Login, Passwort-ändern, Dashboard-Platzhalter,
  Benutzer- und Einstellungsseiten.
- Deploy: nativer LXC-Installer + Update-Skript (systemd), env-Vorlage, Doku.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-19 18:27:42 +02:00
co-authored by Claude Opus 4.8
commit 2c795a57c8
59 changed files with 6419 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
"""Passwort-Hashing (argon2id) und Token-Helfer für Sitzungen.
argon2id ist der aktuelle Empfehlungsstandard und hat – anders als bcrypt – keine
72-Byte-Grenze. Die Sitzungs-Tokens selbst werden nur als SHA-256-Hash gespeichert,
damit ein Blick in die Datenbank keine gültigen Cookies verrät.
"""
import hashlib
import secrets
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError
_hasher = PasswordHasher()
def hash_password(password: str) -> str:
return _hasher.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
try:
return _hasher.verify(password_hash, password)
except (VerifyMismatchError, VerificationError, InvalidHashError, TypeError):
return False
def needs_rehash(password_hash: str) -> bool:
"""True, wenn der Hash mit veralteten Parametern erzeugt wurde."""
try:
return _hasher.check_needs_rehash(password_hash)
except (InvalidHashError, TypeError):
return False
def generate_session_token() -> str:
"""Zufälliges, langlebiges Sitzungs-Token (Klartext, nur im Cookie)."""
return secrets.token_urlsafe(32)
def hash_session_token(token: str) -> str:
"""SHA-256 – schnell genug, um bei jeder Anfrage geprüft zu werden."""
return hashlib.sha256(token.encode("utf-8")).hexdigest()