feat(api): Core-API mit Authentifizierung, CRUD und Monatsreport

- Anmeldung über Argon2id und JWT in httpOnly-Cookies, Refresh mit echter
  Rotation über die neue Tabelle refresh_token
- AuthProvider-Protokoll als Vorbereitung für OIDC, Administrator-Anlage beim
  Erststart mit erzwungenem Passwortwechsel
- CRUD für Konten, Kategorien (zweistufiger Baum), Firmen, Recurrences,
  Preisversionen, Buchungen, Budgets, Vorlagen und Sparziele
- Fälligkeiten mit Overlay-Logik: abrufen, bestätigen, auslassen, zurücksetzen
- Kontosalden zum Stichtag, Monatsübersicht mit Plan-Ist-Vergleich
- SECRET_KEY jetzt mindestens 32 Zeichen; Platzhalter in Produktion abgelehnt
- 61 neue Integrationstests, insgesamt 148 grün

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
moneyfy
2026-09-09 13:40:37 +02:00
co-authored by Claude Opus 5
parent 70d73cf8d3
commit b586d27b77
46 changed files with 4866 additions and 21 deletions
+2 -1
View File
@@ -1,7 +1,7 @@
"""SQLAlchemy-Modelle. Import hier hält Alembics Autogenerate vollständig."""
from app.db.base import Base
from app.models.core import Account, AppUser, Category, LogoAsset, Merchant
from app.models.core import Account, AppUser, Category, LogoAsset, Merchant, RefreshToken
from app.models.enums import (
AccountType,
BusinessDayShift,
@@ -48,6 +48,7 @@ __all__ = [
"Occurrence",
"OccurrenceStatus",
"Recurrence",
"RefreshToken",
"ReserveLedger",
"SavingsGoal",
"Transaction",
+23
View File
@@ -148,3 +148,26 @@ class AppUser(Base, CreatedAtMixin):
name="local_or_external_login",
),
)
class RefreshToken(Base, CreatedAtMixin):
"""Ausgegebenes Refresh-Token.
Für die Rotation beim Refresh wird serverseitiger Zustand benötigt: Ein Token
ist nur gültig, solange seine `jti` hier ungesperrt hinterlegt ist. Beim Refresh
wird der alte Eintrag gesperrt und ein neuer angelegt.
"""
__tablename__ = "refresh_token"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
jti: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("app_user.id", ondelete="CASCADE"), nullable=False
)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
user: Mapped[AppUser] = relationship()
__table_args__ = (Index("ix_refresh_token_user_id", "user_id"),)