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:
@@ -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",
|
||||
|
||||
@@ -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"),)
|
||||
|
||||
Reference in New Issue
Block a user