Sicherheit + Tests + A11y, SW by-v1118

PYDANTIC max_length (38 Routen, ~400 Field-Constraints):
Schützt vor DoS durch Riesen-Payloads (10MB Thread-Titel etc.).
Pragmatische Limits:
- Titel/Name: 200 · Beschreibung/Body: 10000 · Notiz: 5000
- Email: 254 (RFC 5321) · URL: 500 · Slug/Kategorie: 100
- Hund-Name/Rasse: 80 · Hund-Bio: 2000

Top-betroffen: forum.py, diary.py, health.py, dogs.py, expenses.py,
notes.py, auth.py, profile.py. Manuelle len()-Checks in profile,
chat, ki entfernt (jetzt durch Field abgedeckt).

PYTEST COVERAGE (+19 Tests, 37 grün + 1 xfail):
- test_security.py: require_owner (Places GET/PATCH/DELETE mit
  Fremduser → 403), JWT-Blacklist (Logout invalidiert Token),
  Login-Lockout (5 Fehlversuche → 429 + Retry-After Header)
- test_race.py: Invoice-Counter (20 parallele Threads, alle unique),
  Founder-Number (atomare Vergabe, voll bei 100)
- test_validation.py: Forum-Titel 30k Zeichen → 422, Diary-Text
  50k → 422 (verifiziert Pydantic max_length-Sweep)

A11Y (Tap-Targets ≥44×44 + Dark-Mode-Kontrast):
- #header-user-btn 36→44px, .header-back 40→44, .header-menu-btn 40→44
- dog-profile Wrapped-Slider Prev/Next 40→44
- forum-Lightbox Close 40→44
- --c-text-muted Light: #B0A090 (2.37:1 FAIL) → #7F6B58 (4.74:1 PASS)
- --c-text-muted Dark:  #806A58 (3.58:1 FAIL) → #A08878 (5.46:1 PASS)
- Branding-Farben unangetastet
This commit is contained in:
rene 2026-05-27 13:40:30 +02:00
parent 7751d303bb
commit 1ff66a7083
57 changed files with 1253 additions and 612 deletions

View file

@ -4,7 +4,7 @@ import logging
from datetime import date
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from database import db
@ -27,68 +27,68 @@ def _require_breeder(user=Depends(get_current_user)):
# Schemas
# ------------------------------------------------------------------
class LitterCreate(BaseModel):
wurf_rang: Optional[str] = None # A, B, C …
wurf_name: Optional[str] = None # z.B. "Vatertags-Wurf"
vater_name: Optional[str] = None
mutter_name: Optional[str] = None
wurf_rang: Optional[str] = Field(None, max_length=10) # A, B, C …
wurf_name: Optional[str] = Field(None, max_length=200) # z.B. "Vatertags-Wurf"
vater_name: Optional[str] = Field(None, max_length=200)
mutter_name: Optional[str] = Field(None, max_length=200)
vater_id: Optional[int] = None
mutter_id: Optional[int] = None
geburt_datum: Optional[str] = None
erwartetes_datum: Optional[str] = None
geburt_datum: Optional[str] = Field(None, max_length=32)
erwartetes_datum: Optional[str] = Field(None, max_length=32)
welpen_gesamt: Optional[int] = None
welpen_verfuegbar: Optional[int] = None
beschreibung: Optional[str] = None
gesundheitstests: Optional[str] = None
preis_spanne: Optional[str] = None
status: str = "geplant"
beschreibung: Optional[str] = Field(None, max_length=10000)
gesundheitstests: Optional[str] = Field(None, max_length=5000)
preis_spanne: Optional[str] = Field(None, max_length=100)
status: str = Field("geplant", max_length=30)
sichtbar: int = 0
sichtbar_bis: Optional[str] = None
sichtbar_bis: Optional[str] = Field(None, max_length=32)
class LitterUpdate(BaseModel):
wurf_rang: Optional[str] = None
wurf_name: Optional[str] = None
vater_name: Optional[str] = None
mutter_name: Optional[str] = None
wurf_rang: Optional[str] = Field(None, max_length=10)
wurf_name: Optional[str] = Field(None, max_length=200)
vater_name: Optional[str] = Field(None, max_length=200)
mutter_name: Optional[str] = Field(None, max_length=200)
vater_id: Optional[int] = None
mutter_id: Optional[int] = None
geburt_datum: Optional[str] = None
erwartetes_datum: Optional[str] = None
geburt_datum: Optional[str] = Field(None, max_length=32)
erwartetes_datum: Optional[str] = Field(None, max_length=32)
welpen_gesamt: Optional[int] = None
welpen_verfuegbar: Optional[int] = None
beschreibung: Optional[str] = None
gesundheitstests: Optional[str] = None
preis_spanne: Optional[str] = None
status: Optional[str] = None
beschreibung: Optional[str] = Field(None, max_length=10000)
gesundheitstests: Optional[str] = Field(None, max_length=5000)
preis_spanne: Optional[str] = Field(None, max_length=100)
status: Optional[str] = Field(None, max_length=30)
sichtbar: Optional[int] = None
sichtbar_bis: Optional[str] = None
sichtbar_bis: Optional[str] = Field(None, max_length=32)
class PuppyCreate(BaseModel):
name: Optional[str] = None
geschlecht: Optional[str] = None # maennlich|weiblich
farbe: Optional[str] = None
chip_nr: Optional[str] = None
name: Optional[str] = Field(None, max_length=80)
geschlecht: Optional[str] = Field(None, max_length=20) # maennlich|weiblich
farbe: Optional[str] = Field(None, max_length=100)
chip_nr: Optional[str] = Field(None, max_length=50)
geburtsgewicht: Optional[float] = None # Gramm
status: str = "verfuegbar" # verfuegbar|reserviert|abgegeben
status: str = Field("verfuegbar", max_length=30) # verfuegbar|reserviert|abgegeben
status_sichtbar: int = 1
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
class PuppyUpdate(BaseModel):
name: Optional[str] = None
geschlecht: Optional[str] = None
farbe: Optional[str] = None
chip_nr: Optional[str] = None
name: Optional[str] = Field(None, max_length=80)
geschlecht: Optional[str] = Field(None, max_length=20)
farbe: Optional[str] = Field(None, max_length=100)
chip_nr: Optional[str] = Field(None, max_length=50)
geburtsgewicht: Optional[float] = None
status: Optional[str] = None
status: Optional[str] = Field(None, max_length=30)
status_sichtbar: Optional[int] = None
notiz: Optional[str] = None
notiz: Optional[str] = Field(None, max_length=2000)
class WeightEntry(BaseModel):
gewicht_g: float
gemessen_am: str # YYYY-MM-DD
gemessen_am: str = Field(..., max_length=32) # YYYY-MM-DD
# ------------------------------------------------------------------
@ -663,15 +663,15 @@ async def generate_contract(
# Warteliste
# ------------------------------------------------------------------
class WaitlistEntry(BaseModel):
name: str
email: Optional[str] = None
telefon: Optional[str] = None
nachricht: Optional[str] = None
wunsch_geschlecht: str = "egal"
wunsch_farbe: Optional[str] = None
name: str = Field(..., min_length=1, max_length=200)
email: Optional[str] = Field(None, max_length=254)
telefon: Optional[str] = Field(None, max_length=30)
nachricht: Optional[str] = Field(None, max_length=5000)
wunsch_geschlecht: str = Field("egal", max_length=20)
wunsch_farbe: Optional[str] = Field(None, max_length=100)
prioritaet: int = 0
status: str = "anfrage"
notiz: Optional[str] = None
status: str = Field("anfrage", max_length=30)
notiz: Optional[str] = Field(None, max_length=2000)
class WaitlistUpdate(BaseModel):