Wer nach dem Gehaltseingang plant, stellt unter Einstellungen den Tag ein, ab dem ein neuer Monat zählt. Der Zeitraum läuft dann vom Gehaltstag bis zum Vortag des nächsten und trägt den Namen des Monats, in dem er beginnt: Mit dem 25. umfasst „September 2026“ den 25.09. bis zum 24.10. Ein Starttag jenseits der Monatslänge rutscht auf den Monatsletzten, sodass 31 verlässlich den letzten Tag des Monats meint. Dashboard, Cashflow-Kalender, Budgets, Zwölf-Monats-Vorschau, die Kategorienauswertung, der Monatsexport und die Benachrichtigung über überschrittene Budgets rechnen mit diesem Zeitraum. Budgets bleiben je Monat gepflegt; der Bezeichner ist weiterhin der Monatserste, nur der Schnitt verschiebt sich. Bestandsinstallationen bleiben beim Ersten. Die Einstellung liegt in einer einzeiligen Tabelle hinter GET/PUT /api/settings; die Monatsauswertungen liefern zusätzlich period_start und period_end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fj7mB1PGA1aDHyfdSGHgzD
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""Wiederverwendbare FastAPI-Dependencies."""
|
||
|
||
from typing import Annotated
|
||
|
||
from fastapi import Depends, Request
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.cookies import ACCESS_COOKIE
|
||
from app.core.errors import AppError, AuthError
|
||
from app.core.security import decode_token
|
||
from app.db.session import get_session
|
||
from app.models import AppUser
|
||
from app.services.auth import get_user
|
||
from app.services.settings import month_start_day
|
||
|
||
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
||
|
||
|
||
class PasswordChangeRequiredError(AppError):
|
||
"""Der Zugriff ist erst nach dem erzwungenen Passwortwechsel möglich."""
|
||
|
||
status_code = 403
|
||
code = "password_change_required"
|
||
|
||
|
||
async def get_current_user(request: Request, session: DbSession) -> AppUser:
|
||
"""Liest das Access-Token aus dem Cookie und lädt den Benutzer."""
|
||
token = request.cookies.get(ACCESS_COOKIE)
|
||
if not token:
|
||
raise AuthError("Nicht angemeldet.", code="not_authenticated")
|
||
|
||
payload = decode_token(token, "access")
|
||
try:
|
||
user_id = int(payload["sub"])
|
||
except (KeyError, TypeError, ValueError) as exc:
|
||
raise AuthError("Ungültiges Token.", code="invalid_token") from exc
|
||
|
||
user = await get_user(session, user_id)
|
||
if user is None:
|
||
raise AuthError("Der Benutzer existiert nicht mehr.", code="invalid_token")
|
||
return user
|
||
|
||
|
||
CurrentUser = Annotated[AppUser, Depends(get_current_user)]
|
||
|
||
|
||
async def get_active_user(user: CurrentUser) -> AppUser:
|
||
"""Wie `get_current_user`, verlangt aber einen abgeschlossenen Passwortwechsel."""
|
||
if user.must_change_password:
|
||
raise PasswordChangeRequiredError(
|
||
"Das Passwort muss zuerst geändert werden.",
|
||
)
|
||
return user
|
||
|
||
|
||
ActiveUser = Annotated[AppUser, Depends(get_active_user)]
|
||
|
||
|
||
async def get_month_start_day(session: DbSession) -> int:
|
||
"""Der eingestellte Monatsbeginn – jede monatsbezogene Auswertung richtet sich danach."""
|
||
return await month_start_day(session)
|
||
|
||
|
||
MonthStartDay = Annotated[int, Depends(get_month_start_day)]
|