diff --git a/VERSION b/VERSION index 109da83..5568a07 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1117 \ No newline at end of file +1118 \ No newline at end of file diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 713a055..fac5a1c 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -12,7 +12,7 @@ from zoneinfo import ZoneInfo from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import Response -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, List from database import db, DB_PATH from auth import get_current_user @@ -92,15 +92,15 @@ _VALID_TIERS = {"standard", "pro", "breeder", "standard_test", "pro_test", "bree class QuarterlyReportBody(BaseModel): year: int quarter: int - email: str + email: str = Field(..., max_length=254) class UserPatch(BaseModel): - rolle: Optional[str] = None # user | moderator | admin + rolle: Optional[str] = Field(None, max_length=30) # user | moderator | admin is_moderator: Optional[int] = None is_banned: Optional[int] = None - ban_reason: Optional[str] = None + ban_reason: Optional[str] = Field(None, max_length=1000) is_social_media: Optional[int] = None - subscription_tier: Optional[str] = None + subscription_tier: Optional[str] = Field(None, max_length=50) class WikiEnrichBody(BaseModel): limit: int = 10 diff --git a/backend/routes/adoption.py b/backend/routes/adoption.py index 96888c7..d353520 100644 --- a/backend/routes/adoption.py +++ b/backend/routes/adoption.py @@ -16,7 +16,7 @@ import uuid import httpx from datetime import datetime, timedelta from fastapi import APIRouter, Query, BackgroundTasks, Depends, Form, UploadFile, File, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -292,7 +292,7 @@ async def adoption_geocode(plz: str = Query(..., min_length=4, max_length=10)): # ================================================================== class InterestBody(BaseModel): - nachricht: Optional[str] = None + nachricht: Optional[str] = Field(None, max_length=5000) # ------------------------------------------------------------------ @@ -422,7 +422,7 @@ async def community_create( # PATCH /api/adoption/community/{id} — Status ändern (nur Besitzer) # ------------------------------------------------------------------ class _StatusBody(BaseModel): - status: str + status: str = Field(..., max_length=50) @router.patch("/community/{listing_id}") def community_update_status( diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 4a9def8..18b092b 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -10,7 +10,7 @@ from typing import Optional import jwt as _pyjwt from fastapi import APIRouter, HTTPException, Request, Response, Depends from fastapi.responses import RedirectResponse -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, Field from database import db from auth import ( hash_password, verify_password, create_token, @@ -146,13 +146,13 @@ def _send_verification_email(email: str, name: str, token: str): class LoginRequest(BaseModel): email: EmailStr - password: str + password: str = Field(..., min_length=1, max_length=200) class RegisterRequest(BaseModel): email: EmailStr - password: str - name: str - ref_code: Optional[str] = None + password: str = Field(..., min_length=8, max_length=200) + name: str = Field(..., min_length=2, max_length=40) + ref_code: Optional[str] = Field(None, max_length=50) def _gen_referral_code() -> str: @@ -426,8 +426,8 @@ class ForgotPasswordRequest(BaseModel): email: EmailStr class ResetPasswordRequest(BaseModel): - token: str - password: str + token: str = Field(..., min_length=10, max_length=200) + password: str = Field(..., min_length=8, max_length=200) @router.post("/forgot-password") async def forgot_password(data: ForgotPasswordRequest, request: Request): @@ -471,8 +471,8 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request): class UpgradeRequestBody(BaseModel): - tier: str - message: Optional[str] = None + tier: str = Field(..., max_length=50) + message: Optional[str] = Field(None, max_length=2000) @router.post("/upgrade-request") async def create_upgrade_request(data: UpgradeRequestBody, user=Depends(get_current_user)): diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py index fe4028b..e53a1d4 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -6,7 +6,7 @@ from zoneinfo import ZoneInfo from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from fastapi.responses import FileResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db @@ -237,7 +237,7 @@ async def admin_download_document(user_id: int, doc_id: int, admin=Depends(requi class RejectBody(BaseModel): - grund: str + grund: str = Field(..., min_length=3, max_length=2000) # ------------------------------------------------------------------ @@ -483,13 +483,13 @@ async def admin_create_profile(admin=Depends(require_admin)): # PUT /api/breeder/profile — eigenes Profil bearbeiten # ------------------------------------------------------------------ class BreederProfileUpdate(BaseModel): - zwingername: Optional[str] = None - rasse_text: Optional[str] = None - verein: Optional[str] = None + zwingername: Optional[str] = Field(None, max_length=200) + rasse_text: Optional[str] = Field(None, max_length=200) + verein: Optional[str] = Field(None, max_length=200) vdh_mitglied: Optional[int] = None - stadt: Optional[str] = None - website: Optional[str] = None - beschreibung: Optional[str] = None + stadt: Optional[str] = Field(None, max_length=200) + website: Optional[str] = Field(None, max_length=500) + beschreibung: Optional[str] = Field(None, max_length=10000) @router.put("/breeder/profile") async def update_breeder_profile(body: BreederProfileUpdate, user=Depends(require_breeder)): diff --git a/backend/routes/breeder_photos.py b/backend/routes/breeder_photos.py index 18eb085..802440f 100644 --- a/backend/routes/breeder_photos.py +++ b/backend/routes/breeder_photos.py @@ -1,7 +1,7 @@ """BAN YARO — Züchter-Fotos (Upload, Verwaltung, öffentliche Ansicht)""" from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from fastapi.responses import FileResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional import os, logging, asyncio from database import db @@ -30,10 +30,10 @@ def _require_breeder(user=Depends(get_current_user)): # Modelle # ------------------------------------------------------------------ class VisibilityBody(BaseModel): - visibility: str + visibility: str = Field(..., max_length=30) class CaptionBody(BaseModel): - caption: Optional[str] = None + caption: Optional[str] = Field(None, max_length=500) # ------------------------------------------------------------------ diff --git a/backend/routes/chat.py b/backend/routes/chat.py index 0874303..7147315 100644 --- a/backend/routes/chat.py +++ b/backend/routes/chat.py @@ -4,7 +4,7 @@ import os import uuid from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import get_current_user @@ -142,7 +142,7 @@ async def get_messages(conv_id: int, offset: int = 0, limit: int = 50, class SendMsgModel(BaseModel): - text: str + text: str = Field(..., min_length=1, max_length=2000) @router.post("/conversations/{conv_id}/messages", status_code=201) @@ -151,8 +151,6 @@ async def send_message(conv_id: int, data: SendMsgModel, user=Depends(get_curren text = data.text.strip() if not text: raise HTTPException(400, "Nachricht darf nicht leer sein.") - if len(text) > 2000: - raise HTTPException(400, "Nachricht zu lang (max. 2000 Zeichen).") with db() as conn: conv = conn.execute( diff --git a/backend/routes/diary.py b/backend/routes/diary.py index a17bf4d..8d85a84 100644 --- a/backend/routes/diary.py +++ b/backend/routes/diary.py @@ -2,7 +2,7 @@ import os, uuid, json, logging, asyncio from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user, require_admin @@ -20,27 +20,27 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") class DiaryCreate(BaseModel): - datum: Optional[str] = None # ISO date, default heute - client_time: Optional[str] = None # lokale Uhrzeit des Geräts (YYYY-MM-DDTHH:MM:SS) - typ: str = "eintrag" - titel: Optional[str] = None - text: Optional[str] = None + datum: Optional[str] = Field(None, max_length=32) # ISO date, default heute + client_time: Optional[str] = Field(None, max_length=64) # lokale Uhrzeit des Geräts (YYYY-MM-DDTHH:MM:SS) + typ: str = Field("eintrag", max_length=50) + titel: Optional[str] = Field(None, max_length=200) + text: Optional[str] = Field(None, max_length=10000) tags: Optional[list] = None gps_lat: Optional[float] = None gps_lon: Optional[float] = None - location_name: Optional[str] = None + location_name: Optional[str] = Field(None, max_length=300) is_milestone: bool = False dog_ids: Optional[list[int]] = None # alle Hunde inkl. primär; None = nur primary - weather_json: Optional[str] = None # Client-seitig vorab geholtes Wetter (Fallback wenn kein GPS) + weather_json: Optional[str] = Field(None, max_length=5000) # Client-seitig vorab geholtes Wetter (Fallback wenn kein GPS) class DiaryUpdate(BaseModel): - titel: Optional[str] = None - text: Optional[str] = None + titel: Optional[str] = Field(None, max_length=200) + text: Optional[str] = Field(None, max_length=10000) tags: Optional[list] = None gps_lat: Optional[float] = None gps_lon: Optional[float] = None - location_name: Optional[str] = None + location_name: Optional[str] = Field(None, max_length=300) is_milestone: Optional[bool] = None dog_ids: Optional[list[int]] = None # wenn gesetzt: Hunde-Zuweisung ersetzen diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 8004df3..1c37b99 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -3,7 +3,7 @@ import os import uuid from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user, has_pro_access @@ -29,28 +29,28 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") class DogCreate(BaseModel): - name: str - rasse: Optional[str] = None - geburtstag: Optional[str] = None - geschlecht: Optional[str] = None + name: str = Field(..., min_length=1, max_length=80) + rasse: Optional[str] = Field(None, max_length=80) + geburtstag: Optional[str] = Field(None, max_length=32) + geschlecht: Optional[str] = Field(None, max_length=20) gewicht_kg: Optional[float] = None widerrist_cm: Optional[float] = None - chip_nr: Optional[str] = None - bio: Optional[str] = None - is_public: bool = False + chip_nr: Optional[str] = Field(None, max_length=50) + bio: Optional[str] = Field(None, max_length=2000) + is_public: bool = False class DogUpdate(BaseModel): - name: Optional[str] = None - rasse: Optional[str] = None - rasse_id: Optional[int] = None - geburtstag: Optional[str] = None - geschlecht: Optional[str] = None + name: Optional[str] = Field(None, max_length=80) + rasse: Optional[str] = Field(None, max_length=80) + rasse_id: Optional[int] = None + geburtstag: Optional[str] = Field(None, max_length=32) + geschlecht: Optional[str] = Field(None, max_length=20) gewicht_kg: Optional[float] = None widerrist_cm: Optional[float] = None - chip_nr: Optional[str] = None - bio: Optional[str] = None - is_public: Optional[bool] = None + chip_nr: Optional[str] = Field(None, max_length=50) + bio: Optional[str] = Field(None, max_length=2000) + is_public: Optional[bool] = None @router.get("") @@ -1033,8 +1033,8 @@ async def public_dog_profile(dog_id: int): class FoundReport(BaseModel): - message: Optional[str] = None - kontakt: Optional[str] = None + message: Optional[str] = Field(None, max_length=1000) + kontakt: Optional[str] = Field(None, max_length=300) # Gefunden-Meldung (kein Login nötig) @@ -1319,7 +1319,7 @@ async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)): # POST /api/dogs/{id}/gedenken — Hund als verstorben markieren # ------------------------------------------------------------------ class GedenkenData(BaseModel): - verstorben_am: str # YYYY-MM-DD + verstorben_am: str = Field(..., max_length=32) # YYYY-MM-DD @router.post("/{dog_id}/gedenken") async def mark_verstorben(dog_id: int, data: GedenkenData, user=Depends(get_current_user)): diff --git a/backend/routes/ernaehrung.py b/backend/routes/ernaehrung.py index 2aa4760..d485c80 100644 --- a/backend/routes/ernaehrung.py +++ b/backend/routes/ernaehrung.py @@ -2,7 +2,7 @@ import logging from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -16,18 +16,18 @@ logger = logging.getLogger(__name__) # Schemas # ------------------------------------------------------------------ class FutterProfilUpdate(BaseModel): - futter_typ: Optional[str] = None # trocken|nass|barf|mix - marke: Optional[str] = None + futter_typ: Optional[str] = Field(None, max_length=50) # trocken|nass|barf|mix + marke: Optional[str] = Field(None, max_length=200) kcal_tag: Optional[int] = None portionen: Optional[int] = None - notizen: Optional[str] = None + notizen: Optional[str] = Field(None, max_length=5000) class KiBeratungRequest(BaseModel): - frage: str - dog_name: Optional[str] = None - rasse: Optional[str] = None - alter: Optional[str] = None + frage: str = Field(..., min_length=3, max_length=2000) + dog_name: Optional[str] = Field(None, max_length=80) + rasse: Optional[str] = Field(None, max_length=80) + alter: Optional[str] = Field(None, max_length=50) gewicht: Optional[float] = None aktiv: Optional[bool] = None @@ -183,20 +183,20 @@ _GASTRO_HINWEIS = "Magen-Darm-Symptome wie {label} treten meist innerhalb wenige class FutterEintragCreate(BaseModel): - datum: str - uhrzeit: str - futter_name: str - futter_typ: Optional[str] = "trockenfutter" + datum: str = Field(..., max_length=32) + uhrzeit: str = Field(..., max_length=20) + futter_name: str = Field(..., max_length=200) + futter_typ: Optional[str] = Field("trockenfutter", max_length=50) menge_g: Optional[int] = None - notiz: Optional[str] = None + notiz: Optional[str] = Field(None, max_length=2000) class ReaktionCreate(BaseModel): - datum: str - uhrzeit: str - reaktion_typ: str + datum: str = Field(..., max_length=32) + uhrzeit: str = Field(..., max_length=20) + reaktion_typ: str = Field(..., max_length=100) intensitaet: Optional[int] = 3 - notiz: Optional[str] = None + notiz: Optional[str] = Field(None, max_length=2000) # ------------------------------------------------------------------ diff --git a/backend/routes/events.py b/backend/routes/events.py index 0b6de27..f2ca8a6 100644 --- a/backend/routes/events.py +++ b/backend/routes/events.py @@ -2,7 +2,7 @@ from datetime import date from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -17,29 +17,29 @@ TYPEN = {'ausstellung', 'training', 'treffen', 'markt', 'wettkampf', 'sonstiges' # Schemas # ------------------------------------------------------------------ class RsvpCreate(BaseModel): - status: str = 'going' # 'going' | 'maybe' + status: str = Field('going', max_length=20) # 'going' | 'maybe' class EventCreate(BaseModel): - titel: str - datum: str # YYYY-MM-DD - uhrzeit: Optional[str] = None + titel: str = Field(..., min_length=3, max_length=200) + datum: str = Field(..., max_length=32) # YYYY-MM-DD + uhrzeit: Optional[str] = Field(None, max_length=20) lat: Optional[float] = None lon: Optional[float] = None - ort_name: Optional[str] = None - typ: str = 'sonstiges' - beschreibung: Optional[str] = None - link: Optional[str] = None + ort_name: Optional[str] = Field(None, max_length=300) + typ: str = Field('sonstiges', max_length=50) + beschreibung: Optional[str] = Field(None, max_length=10000) + link: Optional[str] = Field(None, max_length=500) class EventUpdate(BaseModel): - titel: Optional[str] = None - datum: Optional[str] = None - uhrzeit: Optional[str] = None + titel: Optional[str] = Field(None, max_length=200) + datum: Optional[str] = Field(None, max_length=32) + uhrzeit: Optional[str] = Field(None, max_length=20) lat: Optional[float] = None lon: Optional[float] = None - ort_name: Optional[str] = None - typ: Optional[str] = None - beschreibung: Optional[str] = None - link: Optional[str] = None + ort_name: Optional[str] = Field(None, max_length=300) + typ: Optional[str] = Field(None, max_length=50) + beschreibung: Optional[str] = Field(None, max_length=10000) + link: Optional[str] = Field(None, max_length=500) # ------------------------------------------------------------------ diff --git a/backend/routes/expenses.py b/backend/routes/expenses.py index 9c93475..9a34f27 100644 --- a/backend/routes/expenses.py +++ b/backend/routes/expenses.py @@ -4,7 +4,7 @@ import logging from datetime import date, timedelta from dateutil.relativedelta import relativedelta from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -20,35 +20,35 @@ KATEGORIEN = {"tierarzt", "futter", "zubehoer", "versicherung", "sitter", "sonst # ------------------------------------------------------------------ class ExpenseCreate(BaseModel): dog_id: Optional[int] = None - kategorie: str + kategorie: str = Field(..., max_length=50) betrag: float - datum: str - notiz: Optional[str] = None + datum: str = Field(..., max_length=32) + notiz: Optional[str] = Field(None, max_length=1000) class ExpenseUpdate(BaseModel): dog_id: Optional[int] = None - kategorie: Optional[str] = None + kategorie: Optional[str] = Field(None, max_length=50) betrag: Optional[float] = None - datum: Optional[str] = None - notiz: Optional[str] = None + datum: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=1000) class RecurringCreate(BaseModel): dog_id: Optional[int] = None - kategorie: str + kategorie: str = Field(..., max_length=50) betrag: float - haeufigkeit: str # monatlich | quartalsweise | jaehrlich - startdatum: str # ISO date - notiz: Optional[str] = None + haeufigkeit: str = Field(..., max_length=30) # monatlich | quartalsweise | jaehrlich + startdatum: str = Field(..., max_length=32) # ISO date + notiz: Optional[str] = Field(None, max_length=1000) class RecurringUpdate(BaseModel): dog_id: Optional[int] = None - kategorie: Optional[str] = None + kategorie: Optional[str] = Field(None, max_length=50) betrag: Optional[float] = None - haeufigkeit: Optional[str] = None - startdatum: Optional[str] = None - notiz: Optional[str] = None + haeufigkeit: Optional[str] = Field(None, max_length=30) + startdatum: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=1000) aktiv: Optional[bool] = None diff --git a/backend/routes/forum.py b/backend/routes/forum.py index 2834ab0..f8ee5e0 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -2,7 +2,7 @@ import os, uuid, json, logging from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user, get_current_user_optional @@ -27,40 +27,40 @@ KATEGORIEN = ['allgemein', 'rasse', 'region', 'gesundheit', 'erziehung', # Schemas # ------------------------------------------------------------------ class ThreadCreate(BaseModel): - kategorie: str = 'allgemein' - titel: str - text: str + kategorie: str = Field('allgemein', max_length=100) + titel: str = Field(..., min_length=3, max_length=200) + text: str = Field(..., min_length=1, max_length=10000) thread_lat: Optional[float] = None thread_lon: Optional[float] = None - thread_ort: Optional[str] = None - client_time: Optional[str] = None + thread_ort: Optional[str] = Field(None, max_length=300) + client_time: Optional[str] = Field(None, max_length=64) class PostCreate(BaseModel): - text: str - client_time: Optional[str] = None + text: str = Field(..., min_length=1, max_length=10000) + client_time: Optional[str] = Field(None, max_length=64) class ThreadPatch(BaseModel): is_pinned: Optional[int] = None is_locked: Optional[int] = None class ThreadUpdate(BaseModel): - titel: Optional[str] = None - text: Optional[str] = None + titel: Optional[str] = Field(None, max_length=200) + text: Optional[str] = Field(None, max_length=10000) thread_lat: Optional[float] = None thread_lon: Optional[float] = None - thread_ort: Optional[str] = None + thread_ort: Optional[str] = Field(None, max_length=300) class PostUpdate(BaseModel): - text: str + text: str = Field(..., min_length=1, max_length=10000) class LikeBody(BaseModel): - target_type: str # 'thread' | 'post' + target_type: str = Field(..., max_length=20) # 'thread' | 'post' target_id: int class ReportBody(BaseModel): - target_type: str + target_type: str = Field(..., max_length=20) target_id: int - grund: str + grund: str = Field(..., min_length=3, max_length=1000) class LocationBody(BaseModel): lat: Optional[float] = None diff --git a/backend/routes/gassi_zeiten.py b/backend/routes/gassi_zeiten.py index 536e1a1..455aa15 100644 --- a/backend/routes/gassi_zeiten.py +++ b/backend/routes/gassi_zeiten.py @@ -3,7 +3,7 @@ import json import logging from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, List from database import db from auth import get_current_user @@ -15,14 +15,14 @@ router = APIRouter() class GassiZeitCreate(BaseModel): - dog_id: Optional[int] = None - wochentage: List[str] # ["mo", "mi", "fr"] - uhrzeit: str # "17:00" - ort_name: Optional[str] = None + dog_id: Optional[int] = None + wochentage: List[str] # ["mo", "mi", "fr"] + uhrzeit: str = Field(..., max_length=20) # "17:00" + ort_name: Optional[str] = Field(None, max_length=300) lat: Optional[float] = None lon: Optional[float] = None radius_m: int = 500 - notiz: Optional[str] = None + notiz: Optional[str] = Field(None, max_length=2000) class GassiZeitUpdate(BaseModel): diff --git a/backend/routes/health.py b/backend/routes/health.py index 34ec5ec..7b9ed35 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -3,7 +3,7 @@ import os, uuid from datetime import date, datetime from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -22,59 +22,59 @@ TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie # Schemas # ------------------------------------------------------------------ class HealthCreate(BaseModel): - typ: str - bezeichnung: Optional[str] = None - datum: str - naechstes: Optional[str] = None - notiz: Optional[str] = None + typ: str = Field(..., max_length=50) + bezeichnung: Optional[str] = Field(None, max_length=200) + datum: str = Field(..., max_length=32) + naechstes: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=5000) # Gewicht wert: Optional[float] = None - einheit: Optional[str] = "kg" + einheit: Optional[str] = Field("kg", max_length=20) # Impfung - charge_nr: Optional[str] = None - tierarzt_name: Optional[str] = None + charge_nr: Optional[str] = Field(None, max_length=100) + tierarzt_name: Optional[str] = Field(None, max_length=200) # Tierarztbesuch kosten: Optional[float] = None - diagnose: Optional[str] = None + diagnose: Optional[str] = Field(None, max_length=2000) # Medikament - dosierung: Optional[str] = None - haeufigkeit: Optional[str] = None + dosierung: Optional[str] = Field(None, max_length=200) + haeufigkeit: Optional[str] = Field(None, max_length=200) aktiv: Optional[int] = 1 - bis_datum: Optional[str] = None + bis_datum: Optional[str] = Field(None, max_length=32) # Allergie - schweregrad: Optional[str] = None # leicht | mittel | schwer - reaktion: Optional[str] = None + schweregrad: Optional[str] = Field(None, max_length=50) # leicht | mittel | schwer + reaktion: Optional[str] = Field(None, max_length=1000) erinnerung: Optional[int] = 1 intervall_tage: Optional[int] = None # Wiederkehrend alle X Tage # Tierarzt-Verknüpfung tierarzt_id: Optional[int] = None # Züchter - deckdatum: Optional[str] = None - wurftermin: Optional[str] = None + deckdatum: Optional[str] = Field(None, max_length=32) + wurftermin: Optional[str] = Field(None, max_length=32) class HealthUpdate(BaseModel): - bezeichnung: Optional[str] = None - datum: Optional[str] = None - naechstes: Optional[str] = None - notiz: Optional[str] = None + bezeichnung: Optional[str] = Field(None, max_length=200) + datum: Optional[str] = Field(None, max_length=32) + naechstes: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=5000) wert: Optional[float] = None - einheit: Optional[str] = None - charge_nr: Optional[str] = None - tierarzt_name: Optional[str] = None + einheit: Optional[str] = Field(None, max_length=20) + charge_nr: Optional[str] = Field(None, max_length=100) + tierarzt_name: Optional[str] = Field(None, max_length=200) kosten: Optional[float] = None - diagnose: Optional[str] = None - dosierung: Optional[str] = None - haeufigkeit: Optional[str] = None + diagnose: Optional[str] = Field(None, max_length=2000) + dosierung: Optional[str] = Field(None, max_length=200) + haeufigkeit: Optional[str] = Field(None, max_length=200) aktiv: Optional[int] = None - bis_datum: Optional[str] = None - schweregrad: Optional[str] = None - reaktion: Optional[str] = None + bis_datum: Optional[str] = Field(None, max_length=32) + schweregrad: Optional[str] = Field(None, max_length=50) + reaktion: Optional[str] = Field(None, max_length=1000) erinnerung: Optional[int] = None intervall_tage: Optional[int] = None tierarzt_id: Optional[int] = None - deckdatum: Optional[str] = None - wurftermin: Optional[str] = None + deckdatum: Optional[str] = Field(None, max_length=32) + wurftermin: Optional[str] = Field(None, max_length=32) # ------------------------------------------------------------------ @@ -390,7 +390,7 @@ async def list_gewicht(dog_id: int, user=Depends(get_current_user)): # POST /api/dogs/{dog_id}/health/symptom-check — KI-Symptomprüfung # ------------------------------------------------------------------ class SymptomCheckRequest(BaseModel): - symptoms: str + symptoms: str = Field(..., min_length=3, max_length=5000) @router.post("/{dog_id}/health/symptom-check") @@ -576,20 +576,20 @@ async def terminvorschlaege(dog_id: int, user=Depends(get_current_user)): # ================================================================== class InsuranceCreate(BaseModel): - anbieter: str - police_nr: Optional[str] = None + anbieter: str = Field(..., min_length=1, max_length=200) + police_nr: Optional[str] = Field(None, max_length=100) jahresbeitrag: Optional[float] = None - kontakt: Optional[str] = None - ablaufdatum: Optional[str] = None - notizen: Optional[str] = None + kontakt: Optional[str] = Field(None, max_length=500) + ablaufdatum: Optional[str] = Field(None, max_length=32) + notizen: Optional[str] = Field(None, max_length=5000) class InsuranceUpdate(BaseModel): - anbieter: Optional[str] = None - police_nr: Optional[str] = None + anbieter: Optional[str] = Field(None, max_length=200) + police_nr: Optional[str] = Field(None, max_length=100) jahresbeitrag: Optional[float] = None - kontakt: Optional[str] = None - ablaufdatum: Optional[str] = None - notizen: Optional[str] = None + kontakt: Optional[str] = Field(None, max_length=500) + ablaufdatum: Optional[str] = Field(None, max_length=32) + notizen: Optional[str] = Field(None, max_length=5000) @router.get("/{dog_id}/insurance") @@ -674,12 +674,12 @@ TRIGGER_LABELS = { class BehaviorCreate(BaseModel): - datum: str - uhrzeit: Optional[str] = None - kategorie: str - intensitaet: int = 3 - trigger: Optional[str] = None - notiz: Optional[str] = None + datum: str = Field(..., max_length=32) + uhrzeit: Optional[str] = Field(None, max_length=20) + kategorie: str = Field(..., max_length=50) + intensitaet: int = 3 + trigger: Optional[str] = Field(None, max_length=200) + notiz: Optional[str] = Field(None, max_length=5000) @router.get("/{dog_id}/behavior") diff --git a/backend/routes/help.py b/backend/routes/help.py index 6551e4d..05803f0 100644 --- a/backend/routes/help.py +++ b/backend/routes/help.py @@ -1,7 +1,7 @@ """BAN YARO — Hilfe / FAQ Routes""" from fastapi import APIRouter, Depends, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user_optional, require_admin @@ -31,17 +31,17 @@ def _load_active_help_articles() -> list[dict]: # Schemas # ------------------------------------------------------------------ class ArticleCreate(BaseModel): - kategorie: str - frage: str - antwort: str - sort_order: int = 0 - aktiv: int = 1 + kategorie: str = Field(..., max_length=100) + frage: str = Field(..., min_length=3, max_length=500) + antwort: str = Field(..., min_length=3, max_length=10000) + sort_order: int = 0 + aktiv: int = 1 class ArticleUpdate(BaseModel): - kategorie: Optional[str] = None - frage: Optional[str] = None - antwort: Optional[str] = None + kategorie: Optional[str] = Field(None, max_length=100) + frage: Optional[str] = Field(None, max_length=500) + antwort: Optional[str] = Field(None, max_length=10000) sort_order: Optional[int] = None aktiv: Optional[int] = None diff --git a/backend/routes/invoices.py b/backend/routes/invoices.py index 1f27759..87d104d 100644 --- a/backend/routes/invoices.py +++ b/backend/routes/invoices.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import Response -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import require_admin import mailer @@ -19,30 +19,30 @@ logger = logging.getLogger(__name__) # Schemas # ------------------------------------------------------------------ class InvoiceItem(BaseModel): - description: str - quantity: float = 1.0 + description: str = Field(..., max_length=500) + quantity: float = 1.0 unit_price: float class InvoiceCreate(BaseModel): user_id: Optional[int] = None - recipient_name: str - recipient_email: str - recipient_address: Optional[str] = None + recipient_name: str = Field(..., max_length=200) + recipient_email: str = Field(..., max_length=254) + recipient_address: Optional[str] = Field(None, max_length=500) items: List[InvoiceItem] discount_pct: Optional[float] = 0.0 - service_period: Optional[str] = None - notes: Optional[str] = None + service_period: Optional[str] = Field(None, max_length=200) + notes: Optional[str] = Field(None, max_length=5000) class PayBody(BaseModel): - paid_at: str + paid_at: str = Field(..., max_length=32) paid_amount: float - notes: Optional[str] = None + notes: Optional[str] = Field(None, max_length=2000) class CancelBody(BaseModel): - reason: str + reason: str = Field(..., min_length=3, max_length=1000) # ------------------------------------------------------------------ diff --git a/backend/routes/ki.py b/backend/routes/ki.py index 2b16cbe..5169634 100644 --- a/backend/routes/ki.py +++ b/backend/routes/ki.py @@ -1,6 +1,6 @@ """BAN YARO — KI Routes""" from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional import ki as ki_module from auth import get_current_user @@ -11,9 +11,9 @@ router = APIRouter() class TrainingRequest(BaseModel): - problem: str - rasse: Optional[str] = None - alter: Optional[str] = None + problem: str = Field(..., min_length=10, max_length=1000) + rasse: Optional[str] = Field(None, max_length=80) + alter: Optional[str] = Field(None, max_length=50) @router.post("/training") @@ -23,8 +23,6 @@ async def ki_training(req: TrainingRequest, request: Request, rl_check(request, max_requests=10, window_seconds=3600, key="ki_training") if not req.problem or len(req.problem.strip()) < 10: raise HTTPException(400, "Bitte beschreibe das Problem genauer.") - if len(req.problem) > 1000: - raise HTTPException(400, "Beschreibung zu lang (max. 1000 Zeichen).") rasse = req.rasse or "unbekannt" alter = req.alter or "unbekannt" @@ -69,10 +67,10 @@ Schreibe klar und strukturiert, ohne unnötigen Fachjargon.""" # POST /ki/tierarzt — KI-Tierarztfragen # ------------------------------------------------------------------ class TierarztRequest(BaseModel): - symptom: str + symptom: str = Field(..., min_length=5, max_length=1000) dog_id: Optional[int] = None - dog_name: Optional[str] = None - rasse: Optional[str] = None + dog_name: Optional[str] = Field(None, max_length=80) + rasse: Optional[str] = Field(None, max_length=80) @router.post("/tierarzt") @@ -81,8 +79,6 @@ async def ki_tierarzt(req: TierarztRequest, request: Request, """KI-Tierarztfragen: Symptombeschreibung → erste Einschätzung.""" if not req.symptom or len(req.symptom.strip()) < 5: raise HTTPException(400, "Bitte beschreibe das Symptom genauer.") - if len(req.symptom) > 1000: - raise HTTPException(400, "Beschreibung zu lang (max. 1000 Zeichen).") # Rate-Limit: max 5 Anfragen pro User pro Tag with db() as conn: @@ -173,10 +169,10 @@ def _log_rasse_request(user_id: int): class BirthdayRequest(BaseModel): dog_id: int - name: str - rasse: Optional[str] = None + name: str = Field(..., max_length=80) + rasse: Optional[str] = Field(None, max_length=80) alter: Optional[int] = None - mode: str = "tomorrow" # "tomorrow" | "today" + mode: str = Field("tomorrow", max_length=20) # "tomorrow" | "today" @router.post("/geburtstag") async def ki_geburtstag(req: BirthdayRequest, request: Request, @@ -368,12 +364,12 @@ Falls kein Hund erkennbar: ist_hund=false und leeres rassen-Array.""" # ------------------------------------------------------------------ class AbschiedRequest(BaseModel): dog_id: int - name: str - rasse: Optional[str] = None + name: str = Field(..., max_length=80) + rasse: Optional[str] = Field(None, max_length=80) km_total: Optional[float] = None diary_count: Optional[int] = None gemeinsam_tage: Optional[int] = None - last_entry_titel: Optional[str] = None + last_entry_titel: Optional[str] = Field(None, max_length=200) @router.post("/abschied") async def ki_abschied(req: AbschiedRequest, request: Request, diff --git a/backend/routes/knigge.py b/backend/routes/knigge.py index 779d023..d824f56 100644 --- a/backend/routes/knigge.py +++ b/backend/routes/knigge.py @@ -1,7 +1,7 @@ """BAN YARO — Hunde-Knigge Routes""" from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user, get_current_user_optional @@ -13,12 +13,12 @@ router = APIRouter() # Schemas # ------------------------------------------------------------------ class VoteRequest(BaseModel): - szenario_id: str - answer: str + szenario_id: str = Field(..., max_length=100) + answer: str = Field(..., max_length=100) class KiRatRequest(BaseModel): - situation: str + situation: str = Field(..., min_length=3, max_length=2000) # ------------------------------------------------------------------ diff --git a/backend/routes/laeufi.py b/backend/routes/laeufi.py index 22189bd..5ca7e22 100644 --- a/backend/routes/laeufi.py +++ b/backend/routes/laeufi.py @@ -1,7 +1,7 @@ """BAN YARO — Läufigkeit, Progesterontests & Trächtigkeit (Züchter)""" from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from datetime import date, timedelta @@ -78,47 +78,47 @@ def _calc_meilensteine(deckdatum_str: str) -> list: # Schemas # ------------------------------------------------------------------ class LaeufiCreate(BaseModel): - beginn: str - ende: Optional[str] = None - notiz: Optional[str] = None + beginn: str = Field(..., max_length=32) + ende: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=2000) class LaeufiUpdate(BaseModel): - beginn: Optional[str] = None - ende: Optional[str] = None - notiz: Optional[str] = None + beginn: Optional[str] = Field(None, max_length=32) + ende: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=2000) class ProgestCreate(BaseModel): - datum: str + datum: str = Field(..., max_length=32) wert: Optional[float] = None - einheit: str = "ng/ml" - labor: Optional[str] = None - notiz: Optional[str] = None + einheit: str = Field("ng/ml", max_length=20) + labor: Optional[str] = Field(None, max_length=200) + notiz: Optional[str] = Field(None, max_length=2000) class ProgestUpdate(BaseModel): - datum: Optional[str] = None + datum: Optional[str] = Field(None, max_length=32) wert: Optional[float] = None - einheit: Optional[str] = None - labor: Optional[str] = None - notiz: Optional[str] = None + einheit: Optional[str] = Field(None, max_length=20) + labor: Optional[str] = Field(None, max_length=200) + notiz: Optional[str] = Field(None, max_length=2000) class DeckCreate(BaseModel): - deckdatum: str + deckdatum: str = Field(..., max_length=32) laeufi_id: Optional[int] = None ruede_id: Optional[int] = None - ruede_name: Optional[str] = None - deckart: str = "natuerlich" + ruede_name: Optional[str] = Field(None, max_length=200) + deckart: str = Field("natuerlich", max_length=50) traechtig: int = 0 - ultraschall_datum: Optional[str] = None - notiz: Optional[str] = None + ultraschall_datum: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=2000) class DeckUpdate(BaseModel): - deckdatum: Optional[str] = None + deckdatum: Optional[str] = Field(None, max_length=32) ruede_id: Optional[int] = None - ruede_name: Optional[str] = None - deckart: Optional[str] = None + ruede_name: Optional[str] = Field(None, max_length=200) + deckart: Optional[str] = Field(None, max_length=50) traechtig: Optional[int] = None - ultraschall_datum: Optional[str] = None - notiz: Optional[str] = None + ultraschall_datum: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=2000) # ------------------------------------------------------------------ diff --git a/backend/routes/litters.py b/backend/routes/litters.py index 09250d8..3294641 100644 --- a/backend/routes/litters.py +++ b/backend/routes/litters.py @@ -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): diff --git a/backend/routes/lost.py b/backend/routes/lost.py index 1522944..065145f 100644 --- a/backend/routes/lost.py +++ b/backend/routes/lost.py @@ -3,7 +3,7 @@ import os, uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -20,13 +20,13 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") # Schemas # ------------------------------------------------------------------ class LostDogCreate(BaseModel): - name: str - rasse: Optional[str] = None - beschreibung: str + name: str = Field(..., min_length=1, max_length=80) + rasse: Optional[str] = Field(None, max_length=80) + beschreibung: str = Field(..., min_length=3, max_length=5000) lat: float lon: float dog_id: Optional[int] = None - client_time: Optional[str] = None + client_time: Optional[str] = Field(None, max_length=64) # ------------------------------------------------------------------ diff --git a/backend/routes/movies.py b/backend/routes/movies.py index da6c682..79b3d95 100644 --- a/backend/routes/movies.py +++ b/backend/routes/movies.py @@ -1,7 +1,7 @@ """BAN YARO — Hunde-Filme Routes""" from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from datetime import datetime from database import db @@ -207,31 +207,31 @@ class HundDesMonatsVoteRequest(BaseModel): dog_id: int class MovieCreate(BaseModel): - id: str - titel: str - originaltitel: Optional[str] = None + id: str = Field(..., max_length=100) + titel: str = Field(..., min_length=1, max_length=200) + originaltitel: Optional[str] = Field(None, max_length=200) jahr: Optional[int] = None - genre: Optional[str] = None - typ: str = "film" - hund_rasse: Optional[str] = None - stirbt_der_hund: bool = False - beschreibung: Optional[str] = None - bild_emoji: str = "🐾" + genre: Optional[str] = Field(None, max_length=100) + typ: str = Field("film", max_length=30) + hund_rasse: Optional[str] = Field(None, max_length=200) + stirbt_der_hund: bool = False + beschreibung: Optional[str] = Field(None, max_length=5000) + bild_emoji: str = Field("🐾", max_length=10) imdb_rating: Optional[float] = None - streaming: Optional[str] = None + streaming: Optional[str] = Field(None, max_length=500) class MovieUpdate(BaseModel): - titel: Optional[str] = None - originaltitel: Optional[str] = None + titel: Optional[str] = Field(None, max_length=200) + originaltitel: Optional[str] = Field(None, max_length=200) jahr: Optional[int] = None - genre: Optional[str] = None - typ: Optional[str] = None - hund_rasse: Optional[str] = None + genre: Optional[str] = Field(None, max_length=100) + typ: Optional[str] = Field(None, max_length=30) + hund_rasse: Optional[str] = Field(None, max_length=200) stirbt_der_hund: Optional[bool] = None - beschreibung: Optional[str] = None - bild_emoji: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=5000) + bild_emoji: Optional[str] = Field(None, max_length=10) imdb_rating: Optional[float] = None - streaming: Optional[str] = None + streaming: Optional[str] = Field(None, max_length=500) # ------------------------------------------------------------------ diff --git a/backend/routes/notes.py b/backend/routes/notes.py index 6901f62..5a70c2b 100644 --- a/backend/routes/notes.py +++ b/backend/routes/notes.py @@ -4,7 +4,7 @@ import json import logging from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, Any, List from database import db from auth import get_current_user @@ -18,18 +18,18 @@ logger = logging.getLogger(__name__) # Schemas # ------------------------------------------------------------------ class NoteCreate(BaseModel): - text: str - meta_json: Optional[Any] = None - location_name: Optional[str] = None - parent_label: Optional[str] = None - client_time: Optional[str] = None + text: str = Field(..., min_length=1, max_length=5000) + meta_json: Optional[Any] = None + location_name: Optional[str] = Field(None, max_length=300) + parent_label: Optional[str] = Field(None, max_length=200) + client_time: Optional[str] = Field(None, max_length=64) class NoteUpdate(BaseModel): - text: Optional[str] = None + text: Optional[str] = Field(None, max_length=5000) meta_json: Optional[Any] = None - location_name: Optional[str] = None - parent_label: Optional[str] = None + location_name: Optional[str] = Field(None, max_length=300) + parent_label: Optional[str] = Field(None, max_length=200) # ------------------------------------------------------------------ diff --git a/backend/routes/osm.py b/backend/routes/osm.py index 4d1f36f..5fc22b9 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -9,7 +9,7 @@ import httpx import logging from typing import Optional from fastapi import APIRouter, Query, BackgroundTasks, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import get_current_user, get_current_user_optional as get_optional_user @@ -273,11 +273,11 @@ async def get_pois( # POST /user-poi — Community-Marker setzen # ------------------------------------------------------------------ class UserPoiIn(BaseModel): - type: str + type: str = Field(..., max_length=200) lat: float lon: float - name: Optional[str] = None - notiz: Optional[str] = None + name: Optional[str] = Field(None, max_length=300) + notiz: Optional[str] = Field(None, max_length=2000) ALLOWED_TYPES = { 'waste_basket', 'drinking_water', 'dog_park', @@ -331,8 +331,8 @@ async def delete_user_poi(poi_id: int, user = Depends(get_current_user)): # POST /report — Marker als ungültig melden # ------------------------------------------------------------------ class ReportIn(BaseModel): - type: str - grund: str + type: str = Field(..., max_length=100) + grund: str = Field(..., max_length=200) osm_id: Optional[int] = None user_poi_id: Optional[int] = None @@ -388,9 +388,9 @@ async def analyze_region( # POST /pois/{osm_id}/edit — Nutzer schlägt Korrektur vor # ------------------------------------------------------------------ class PoiEditCreate(BaseModel): - poi_name: str - field: str = 'opening_hours' - new_value: str + poi_name: str = Field(..., max_length=300) + field: str = Field('opening_hours', max_length=50) + new_value: str = Field(..., max_length=1000) @router.post('/pois/{osm_id}/edit', status_code=201) diff --git a/backend/routes/outreach.py b/backend/routes/outreach.py index d738998..22139e9 100644 --- a/backend/routes/outreach.py +++ b/backend/routes/outreach.py @@ -13,7 +13,7 @@ from typing import List, Optional import logging from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from auth import require_admin from database import db @@ -135,26 +135,26 @@ def _send_smtp(to: str, subject: str, body: str, account: str = "partner", html: # ------------------------------------------------------------------ class TemplateIn(BaseModel): - key: str - label: str - subject: str - body: str - from_account: str = "partner" + key: str = Field(..., max_length=100) + label: str = Field(..., max_length=200) + subject: str = Field(..., max_length=500) + body: str = Field(..., max_length=50000) + from_account: str = Field("partner", max_length=50) class TemplateUpdate(BaseModel): - label: str - subject: str - body: str - from_account: str = "partner" + label: str = Field(..., max_length=200) + subject: str = Field(..., max_length=500) + body: str = Field(..., max_length=50000) + from_account: str = Field("partner", max_length=50) class SendRequest(BaseModel): to: List[str] - subject: str - body: str - from_account: str = "partner" - template_id: Optional[int] = None + subject: str = Field(..., max_length=500) + body: str = Field(..., max_length=50000) + from_account: str = Field("partner", max_length=50) + template_id: Optional[int] = None # ------------------------------------------------------------------ diff --git a/backend/routes/partner.py b/backend/routes/partner.py index b2d21f0..16caafb 100644 --- a/backend/routes/partner.py +++ b/backend/routes/partner.py @@ -2,7 +2,7 @@ from typing import Optional from fastapi import APIRouter, HTTPException, Depends -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import require_admin, get_current_user @@ -10,8 +10,8 @@ router = APIRouter() class PartnerCodeCreate(BaseModel): - code: str - label: str + code: str = Field(..., min_length=1, max_length=50) + label: str = Field(..., min_length=1, max_length=200) grants_founder: int = 1 max_uses: Optional[int] = None diff --git a/backend/routes/passport.py b/backend/routes/passport.py index 884e8d3..733b367 100644 --- a/backend/routes/passport.py +++ b/backend/routes/passport.py @@ -5,7 +5,7 @@ import secrets from datetime import date, datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -17,25 +17,25 @@ router = APIRouter() # Schemas # ------------------------------------------------------------------ class PassportMeta(BaseModel): - blutgruppe: Optional[str] = None - allergien: Optional[str] = None - besonderheiten: Optional[str] = None + blutgruppe: Optional[str] = Field(None, max_length=50) + allergien: Optional[str] = Field(None, max_length=2000) + besonderheiten: Optional[str] = Field(None, max_length=2000) class VaccinationCreate(BaseModel): - krankheit: str - datum: str - naechste: Optional[str] = None - tierarzt: Optional[str] = None - charge_nr: Optional[str] = None + krankheit: str = Field(..., max_length=200) + datum: str = Field(..., max_length=32) + naechste: Optional[str] = Field(None, max_length=32) + tierarzt: Optional[str] = Field(None, max_length=200) + charge_nr: Optional[str] = Field(None, max_length=100) class MedicationCreate(BaseModel): - name: str - dosierung: Optional[str] = None - von: Optional[str] = None - bis: Optional[str] = None - notiz: Optional[str] = None + name: str = Field(..., max_length=200) + dosierung: Optional[str] = Field(None, max_length=200) + von: Optional[str] = Field(None, max_length=32) + bis: Optional[str] = Field(None, max_length=32) + notiz: Optional[str] = Field(None, max_length=2000) # ------------------------------------------------------------------ diff --git a/backend/routes/places.py b/backend/routes/places.py index 3dfc0a8..12570c0 100644 --- a/backend/routes/places.py +++ b/backend/routes/places.py @@ -1,7 +1,7 @@ """BAN YARO — Hundefreundliche Orte""" from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user, require_owner @@ -16,25 +16,25 @@ TYPEN = {'restaurant', 'shop', 'freilauf', 'kotbeutel', 'tierarzt', 'hundesalon' # Schemas # ------------------------------------------------------------------ class PlaceCreate(BaseModel): - name: str - typ: str + name: str = Field(..., min_length=1, max_length=200) + typ: str = Field(..., max_length=50) lat: float lon: float - adresse: Optional[str] = None - website: Optional[str] = None - telefon: Optional[str] = None + adresse: Optional[str] = Field(None, max_length=300) + website: Optional[str] = Field(None, max_length=500) + telefon: Optional[str] = Field(None, max_length=30) hund_rein: Optional[bool] = None leine_pflicht: Optional[bool] = None wasser_fuer_hunde: Optional[bool] = None class PlaceUpdate(BaseModel): - name: Optional[str] = None - typ: Optional[str] = None + name: Optional[str] = Field(None, max_length=200) + typ: Optional[str] = Field(None, max_length=50) lat: Optional[float]= None lon: Optional[float]= None - adresse: Optional[str] = None - website: Optional[str] = None - telefon: Optional[str] = None + adresse: Optional[str] = Field(None, max_length=300) + website: Optional[str] = Field(None, max_length=500) + telefon: Optional[str] = Field(None, max_length=30) hund_rein: Optional[bool] = None leine_pflicht: Optional[bool] = None wasser_fuer_hunde: Optional[bool] = None diff --git a/backend/routes/playdate.py b/backend/routes/playdate.py index 60f1d2a..2f7ebdd 100644 --- a/backend/routes/playdate.py +++ b/backend/routes/playdate.py @@ -2,7 +2,7 @@ import logging from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -40,18 +40,18 @@ class ListingUpsert(BaseModel): dog_id: int lat: float lon: float - ort_name: Optional[str] = None + ort_name: Optional[str] = Field(None, max_length=300) radius_km: int = 10 - beschreibung: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=2000) class RequestCreate(BaseModel): to_dog_id: int - nachricht: Optional[str] = None + nachricht: Optional[str] = Field(None, max_length=2000) class RequestPatch(BaseModel): - status: str # accepted | declined + status: str = Field(..., max_length=30) # accepted | declined # ------------------------------------------------------------------ diff --git a/backend/routes/poison.py b/backend/routes/poison.py index 15fe392..97d0ed2 100644 --- a/backend/routes/poison.py +++ b/backend/routes/poison.py @@ -3,7 +3,7 @@ import os, uuid from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -22,12 +22,12 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") class PoisonCreate(BaseModel): lat: float lon: float - beschreibung: Optional[str] = None - typ: str = "unbekannt" + beschreibung: Optional[str] = Field(None, max_length=2000) + typ: str = Field("unbekannt", max_length=50) class PoisonResolve(BaseModel): - grund: str = "beseitigt" # beseitigt | fehlerhaft | anderes + grund: str = Field("beseitigt", max_length=50) # beseitigt | fehlerhaft | anderes # ------------------------------------------------------------------ diff --git a/backend/routes/profile.py b/backend/routes/profile.py index baa196c..2fe0e1d 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -7,7 +7,7 @@ import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from auth import get_current_user from database import db @@ -20,17 +20,17 @@ VALID_SICHTBARKEIT = {"public", "friends", "private"} class ProfileUpdate(BaseModel): - real_name: Optional[str] = None - bio: Optional[str] = None - wohnort: Optional[str] = None - erfahrung: Optional[str] = None - social_link: Optional[str] = None - profil_sichtbarkeit: Optional[str] = None + real_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = Field(None, max_length=300) + wohnort: Optional[str] = Field(None, max_length=60) + erfahrung: Optional[str] = Field(None, max_length=30) + social_link: Optional[str] = Field(None, max_length=120) + profil_sichtbarkeit: Optional[str] = Field(None, max_length=30) notes_ki_enabled: Optional[int] = None gassi_stunde_push: Optional[int] = None - preferred_theme: Optional[str] = None - billing_address: Optional[str] = None - geburtstag: Optional[str] = None + preferred_theme: Optional[str] = Field(None, max_length=20) + billing_address: Optional[str] = Field(None, max_length=500) + geburtstag: Optional[str] = Field(None, max_length=10) def _load_user(user_id: int) -> dict: @@ -61,12 +61,7 @@ async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)): raise HTTPException(400, f"profil_sichtbarkeit muss eines von {sorted(VALID_SICHTBARKEIT)} sein.") if "preferred_theme" in fields and fields["preferred_theme"] not in ("system", "light", "dark"): raise HTTPException(400, "preferred_theme muss 'system', 'light' oder 'dark' sein.") - if "bio" in fields and len(fields["bio"]) > 300: - raise HTTPException(400, "bio darf maximal 300 Zeichen lang sein.") - if "wohnort" in fields and len(fields["wohnort"]) > 60: - raise HTTPException(400, "wohnort darf maximal 60 Zeichen lang sein.") - if "social_link" in fields and len(fields["social_link"]) > 120: - raise HTTPException(400, "social_link darf maximal 120 Zeichen lang sein.") + # Längen-Begrenzungen sind jetzt via Field max_length im Schema abgedeckt. if "geburtstag" in fields and fields["geburtstag"]: if not re.fullmatch(r"\d{2}\.\d{2}", fields["geburtstag"]): raise HTTPException(400, "geburtstag muss im Format TT.MM sein (z.B. 16.05).") diff --git a/backend/routes/push.py b/backend/routes/push.py index 3ee73d9..19dbb32 100644 --- a/backend/routes/push.py +++ b/backend/routes/push.py @@ -4,7 +4,7 @@ import os import json import logging from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from pywebpush import webpush, WebPushException @@ -33,7 +33,7 @@ async def get_vapid_key(): # POST /api/push/subscribe — Subscription speichern # ------------------------------------------------------------------ class PushSubscription(BaseModel): - endpoint: str + endpoint: str = Field(..., max_length=2000) keys: dict # { p256dh, auth } expirationTime: Optional[int] = None diff --git a/backend/routes/ratings.py b/backend/routes/ratings.py index dba63f5..cf8a733 100644 --- a/backend/routes/ratings.py +++ b/backend/routes/ratings.py @@ -1,7 +1,7 @@ """BAN YARO — Bewertungssystem (Ratings)""" from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -23,10 +23,10 @@ TABLE_MAP = { # Schemas # ------------------------------------------------------------------ class RatingCreate(BaseModel): - target_type: str + target_type: str = Field(..., max_length=50) target_id: int stars: int - kommentar: Optional[str] = None + kommentar: Optional[str] = Field(None, max_length=5000) # ------------------------------------------------------------------ diff --git a/backend/routes/routen.py b/backend/routes/routen.py index 57c345d..0e81704 100644 --- a/backend/routes/routen.py +++ b/backend/routes/routen.py @@ -5,7 +5,7 @@ import json, os, uuid import httpx import polyline as _polyline from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, List from database import db from auth import get_current_user, get_current_user_optional @@ -37,29 +37,29 @@ class GPSPoint(BaseModel): alt: Optional[float] = None class RouteCreate(BaseModel): - name: str - beschreibung: Optional[str] = None + name: str = Field(..., min_length=1, max_length=200) + beschreibung: Optional[str] = Field(None, max_length=5000) gps_track: List[GPSPoint] distanz_km: Optional[float] = None dauer_min: Optional[int] = None - schwierigkeit: Optional[str] = "leicht" # leicht | mittel | anspruchsvoll - untergrund: Optional[str] = None # wald | asphalt | wiese | mix + schwierigkeit: Optional[str] = Field("leicht", max_length=30) # leicht | mittel | anspruchsvoll + untergrund: Optional[str] = Field(None, max_length=50) # wald | asphalt | wiese | mix schatten: Optional[bool] = None leine_empfohlen: Optional[bool] = None is_public: Optional[bool] = False - hunde_tauglichkeit: Optional[str] = None # eingeschränkt | gut | sehr_gut | premium - client_time: Optional[str] = None + hunde_tauglichkeit: Optional[str] = Field(None, max_length=50) # eingeschränkt | gut | sehr_gut | premium + client_time: Optional[str] = Field(None, max_length=64) dog_ids: Optional[List[int]] = None # Welche Hunde mitgegangen sind class RouteUpdate(BaseModel): - name: Optional[str] = None - beschreibung: Optional[str] = None - schwierigkeit: Optional[str] = None - untergrund: Optional[str] = None + name: Optional[str] = Field(None, max_length=200) + beschreibung: Optional[str] = Field(None, max_length=5000) + schwierigkeit: Optional[str] = Field(None, max_length=30) + untergrund: Optional[str] = Field(None, max_length=50) schatten: Optional[bool] = None leine_empfohlen: Optional[bool] = None is_public: Optional[bool] = None - hunde_tauglichkeit: Optional[str] = None + hunde_tauglichkeit: Optional[str] = Field(None, max_length=50) class RouteDogs(BaseModel): dog_ids: List[int] @@ -553,7 +553,7 @@ async def add_route_photo( # POST /api/routes/{id}/feedback — Feedback an Route-Ersteller # ------------------------------------------------------------------ class RouteFeedback(BaseModel): - text: str + text: str = Field(..., min_length=5, max_length=2000) @router.post("/{route_id}/feedback", status_code=201) async def route_feedback(route_id: int, data: RouteFeedback, user=Depends(get_current_user)): diff --git a/backend/routes/services.py b/backend/routes/services.py index a0409e9..2d1d0fc 100644 --- a/backend/routes/services.py +++ b/backend/routes/services.py @@ -1,7 +1,7 @@ """BAN YARO — Service-Angebote (Sitting & Walks Matching)""" from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -16,8 +16,8 @@ ALLOWED_TYPES = {'sitting', 'walks'} # Schemas # ------------------------------------------------------------------ class ServiceCreate(BaseModel): - type: str - beschreibung: Optional[str] = None + type: str = Field(..., max_length=30) + beschreibung: Optional[str] = Field(None, max_length=5000) preis_pro_tag: Optional[float] = None lat: Optional[float] = None lon: Optional[float] = None diff --git a/backend/routes/sharing.py b/backend/routes/sharing.py index eb2aa58..762c8cd 100644 --- a/backend/routes/sharing.py +++ b/backend/routes/sharing.py @@ -2,7 +2,7 @@ import secrets from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import get_current_user @@ -14,7 +14,7 @@ share_router = APIRouter() class ShareInvite(BaseModel): - role: str = "editor" # viewer | editor + role: str = Field("editor", max_length=20) # viewer | editor # ------------------------------------------------------------------ diff --git a/backend/routes/sitting.py b/backend/routes/sitting.py index dc2e96c..760d942 100644 --- a/backend/routes/sitting.py +++ b/backend/routes/sitting.py @@ -2,7 +2,7 @@ import json from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, List from database import db from auth import get_current_user @@ -17,7 +17,7 @@ SERVICES = {'tagesbetreuung', 'uebernachtung', 'gassi', 'hausbesuch'} # Schemas # ------------------------------------------------------------------ class SitterCreate(BaseModel): - beschreibung: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=5000) preis_pro_tag: float = 0 max_hunde: int = 1 lat: Optional[float] = None @@ -26,7 +26,7 @@ class SitterCreate(BaseModel): services: List[str] = [] class SitterUpdate(BaseModel): - beschreibung: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=5000) preis_pro_tag: Optional[float] = None max_hunde: Optional[int] = None lat: Optional[float] = None @@ -38,12 +38,12 @@ class SitterUpdate(BaseModel): class RequestCreate(BaseModel): sitter_id: int dog_ids: List[int] = [] - von: str # YYYY-MM-DD - bis: str - nachricht: Optional[str] = None + von: str = Field(..., max_length=32) # YYYY-MM-DD + bis: str = Field(..., max_length=32) + nachricht: Optional[str] = Field(None, max_length=2000) class RequestUpdate(BaseModel): - status: str # angenommen | abgelehnt | abgebrochen + status: str = Field(..., max_length=30) # angenommen | abgelehnt | abgebrochen # ------------------------------------------------------------------ diff --git a/backend/routes/sitting_access.py b/backend/routes/sitting_access.py index b49e681..7bbc6da 100644 --- a/backend/routes/sitting_access.py +++ b/backend/routes/sitting_access.py @@ -1,7 +1,7 @@ """BAN YARO — Gasthund-Zugang (Sitter-Subscriptions)""" from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import get_current_user @@ -11,7 +11,7 @@ router = APIRouter() class AccessCreate(BaseModel): dog_id: int sitter_id: int - valid_until: str # 'YYYY-MM-DD' + valid_until: str = Field(..., max_length=32) # 'YYYY-MM-DD' @router.post("", status_code=201) diff --git a/backend/routes/social.py b/backend/routes/social.py index 1cf204d..10db2d9 100644 --- a/backend/routes/social.py +++ b/backend/routes/social.py @@ -9,7 +9,7 @@ import random from typing import Optional from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from auth import get_current_user, require_social_media from database import db @@ -849,24 +849,24 @@ Antworte NUR mit einem JSON-Objekt: class GenerateRequest(BaseModel): - platform: str = "both" - format: str = "post" - topic: str + platform: str = Field("both", max_length=30) + format: str = Field("post", max_length=30) + topic: str = Field(..., min_length=2, max_length=500) breed_id: Optional[int] = None class EvaluateRequest(BaseModel): - platform: str = "instagram" - format: str = "post" - draft: str + platform: str = Field("instagram", max_length=30) + format: str = Field("post", max_length=30) + draft: str = Field(..., min_length=1, max_length=10000) class StatusUpdate(BaseModel): - status: Optional[str] = None - scheduled_at: Optional[str] = None - published_at: Optional[str] = None - notes: Optional[str] = None - post_url: Optional[str] = None + status: Optional[str] = Field(None, max_length=50) + scheduled_at: Optional[str] = Field(None, max_length=64) + published_at: Optional[str] = Field(None, max_length=64) + notes: Optional[str] = Field(None, max_length=5000) + post_url: Optional[str] = Field(None, max_length=500) def _used_topics(limit: int = 30) -> str: diff --git a/backend/routes/tieraerzte.py b/backend/routes/tieraerzte.py index 8448478..65b1c5e 100644 --- a/backend/routes/tieraerzte.py +++ b/backend/routes/tieraerzte.py @@ -2,7 +2,7 @@ import math from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user @@ -11,20 +11,20 @@ router = APIRouter() class TierarztCreate(BaseModel): - name: str - strasse: Optional[str] = None - plz: Optional[str] = None - ort: Optional[str] = None - telefon: Optional[str] = None - notfall_telefon: Optional[str] = None - email: Optional[str] = None - website: Optional[str] = None - notizen: Optional[str] = None + name: str = Field(..., min_length=1, max_length=200) + strasse: Optional[str] = Field(None, max_length=300) + plz: Optional[str] = Field(None, max_length=20) + ort: Optional[str] = Field(None, max_length=200) + telefon: Optional[str] = Field(None, max_length=30) + notfall_telefon: Optional[str] = Field(None, max_length=30) + email: Optional[str] = Field(None, max_length=254) + website: Optional[str] = Field(None, max_length=500) + notizen: Optional[str] = Field(None, max_length=5000) ist_notfallpraxis: bool = False - opening_hours: Optional[str] = None + opening_hours: Optional[str] = Field(None, max_length=500) lat: Optional[float] = None lon: Optional[float] = None - osm_id: Optional[str] = None + osm_id: Optional[str] = Field(None, max_length=100) class BewertungCreate(BaseModel): @@ -32,25 +32,25 @@ class BewertungCreate(BaseModel): wartezeit: Optional[int] = None freundlichkeit: Optional[int] = None kompetenz: Optional[int] = None - text: Optional[str] = None + text: Optional[str] = Field(None, max_length=5000) class TierarztUpdate(BaseModel): - name: Optional[str] = None - strasse: Optional[str] = None - plz: Optional[str] = None - ort: Optional[str] = None - telefon: Optional[str] = None - notfall_telefon: Optional[str] = None - email: Optional[str] = None - website: Optional[str] = None - notizen: Optional[str] = None + name: Optional[str] = Field(None, max_length=200) + strasse: Optional[str] = Field(None, max_length=300) + plz: Optional[str] = Field(None, max_length=20) + ort: Optional[str] = Field(None, max_length=200) + telefon: Optional[str] = Field(None, max_length=30) + notfall_telefon: Optional[str] = Field(None, max_length=30) + email: Optional[str] = Field(None, max_length=254) + website: Optional[str] = Field(None, max_length=500) + notizen: Optional[str] = Field(None, max_length=5000) ist_notfallpraxis: Optional[bool] = None aktiv: Optional[bool] = None - opening_hours: Optional[str] = None + opening_hours: Optional[str] = Field(None, max_length=500) lat: Optional[float] = None lon: Optional[float] = None - osm_id: Optional[str] = None + osm_id: Optional[str] = Field(None, max_length=100) def _fmt_opening_hours(raw: str | None) -> str | None: diff --git a/backend/routes/training.py b/backend/routes/training.py index 078ceef..5903b6f 100644 --- a/backend/routes/training.py +++ b/backend/routes/training.py @@ -1,7 +1,7 @@ """BAN YARO — Übungs- & Trainingsfortschritt""" from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional import datetime import ki @@ -61,9 +61,9 @@ async def get_exercises(): # Admin: Übung bearbeiten (beschreibung / schritte / tipp) # ------------------------------------------------------------------ class ExerciseUpdate(BaseModel): - beschreibung: Optional[str] = None - schritte: Optional[str] = None # JSON-String: '["Schritt 1", ...]' - tipp: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=10000) + schritte: Optional[str] = Field(None, max_length=10000) # JSON-String: '["Schritt 1", ...]' + tipp: Optional[str] = Field(None, max_length=5000) @router.put("/exercises/{exercise_id}") async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(require_admin)): @@ -93,9 +93,9 @@ async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(requ # Übungs-Status # ------------------------------------------------------------------ class ProgressUpdate(BaseModel): - exercise_id: str - status: Optional[str] = None - dog_id: Optional[int] = None + exercise_id: str = Field(..., max_length=200) + status: Optional[str] = Field(None, max_length=50) + dog_id: Optional[int] = None @router.get("/progress") async def get_progress(dog_id: Optional[int] = None, user=Depends(get_current_user)): @@ -137,9 +137,9 @@ async def upsert_progress(body: ProgressUpdate, user=Depends(get_current_user)): # Trainingsplan-Checkboxen # ------------------------------------------------------------------ class PlanProgress(BaseModel): - item_key: str - checked: bool - dog_id: Optional[int] = None + item_key: str = Field(..., max_length=200) + checked: bool + dog_id: Optional[int] = None @router.get("/plan-progress") async def get_plan_progress(dog_id: Optional[int] = None, user=Depends(get_current_user)): @@ -327,15 +327,15 @@ def _check_badges(conn, user_id: int, dog_name: str) -> list: class SessionCreate(BaseModel): dog_id: int - exercise_id: str - exercise_name: str - datum: Optional[str] = None - wiederholungen: int = 1 - erfolgsquote: int = 50 - hund_stimmung: Optional[str] = "aufmerksam" + exercise_id: str = Field(..., max_length=200) + exercise_name: str = Field(..., max_length=200) + datum: Optional[str] = Field(None, max_length=32) + wiederholungen: int = 1 + erfolgsquote: int = 50 + hund_stimmung: Optional[str] = Field("aufmerksam", max_length=50) zufriedenheit: Optional[int] = 3 - notiz: Optional[str] = None - tagebuch_eintrag: bool = False # ignoriert — Training hat eigenes Protokoll + notiz: Optional[str] = Field(None, max_length=2000) + tagebuch_eintrag: bool = False # ignoriert — Training hat eigenes Protokoll @router.post("/sessions") diff --git a/backend/routes/walks.py b/backend/routes/walks.py index 83f6221..07dbada 100644 --- a/backend/routes/walks.py +++ b/backend/routes/walks.py @@ -4,7 +4,7 @@ import os, uuid import httpx from datetime import date from fastapi import APIRouter, Depends, HTTPException, UploadFile, File -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, List from database import db from auth import get_current_user @@ -20,24 +20,24 @@ router = APIRouter() # Schemas # ------------------------------------------------------------------ class WalkCreate(BaseModel): - titel: str - datum: str # YYYY-MM-DD - uhrzeit: str # HH:MM + titel: str = Field(..., min_length=1, max_length=200) + datum: str = Field(..., max_length=32) # YYYY-MM-DD + uhrzeit: str = Field(..., max_length=20) # HH:MM lat: float lon: float - ort_name: Optional[str] = None + ort_name: Optional[str] = Field(None, max_length=300) max_teilnehmer: int = 10 - beschreibung: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=5000) class WalkUpdate(BaseModel): - titel: Optional[str] = None - datum: Optional[str] = None - uhrzeit: Optional[str] = None + titel: Optional[str] = Field(None, max_length=200) + datum: Optional[str] = Field(None, max_length=32) + uhrzeit: Optional[str] = Field(None, max_length=20) lat: Optional[float] = None lon: Optional[float] = None - ort_name: Optional[str] = None + ort_name: Optional[str] = Field(None, max_length=300) max_teilnehmer: Optional[int] = None - beschreibung: Optional[str] = None + beschreibung: Optional[str] = Field(None, max_length=5000) class JoinRequest(BaseModel): dog_ids: List[int] = [] # leere Liste = ohne Hund (selten) @@ -46,7 +46,7 @@ class InviteRequest(BaseModel): friend_id: int class RsvpRequest(BaseModel): - status: str # 'yes' | 'maybe' | 'no' + status: str = Field(..., max_length=20) # 'yes' | 'maybe' | 'no' # ------------------------------------------------------------------ diff --git a/backend/routes/wiki.py b/backend/routes/wiki.py index a05bb1b..7af856c 100644 --- a/backend/routes/wiki.py +++ b/backend/routes/wiki.py @@ -6,7 +6,7 @@ import time import logging from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, UploadFile, File from fastapi.responses import JSONResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from database import db from auth import get_current_user, get_current_user_optional from ratelimit import check as rl_check, block_ip @@ -36,9 +36,9 @@ async def honeypot(request: Request): # Schemas # ------------------------------------------------------------------ class BerichtCreate(BaseModel): - rasse: str - titel: str - text: str + rasse: str = Field(..., max_length=100) + titel: str = Field(..., min_length=3, max_length=200) + text: str = Field(..., min_length=10, max_length=10000) # ------------------------------------------------------------------ @@ -411,8 +411,8 @@ async def list_submissions(user=Depends(get_current_user)): # PATCH /api/wiki/foto-submissions/{id} — genehmigen oder ablehnen # ------------------------------------------------------------------ class ReviewModel(BaseModel): - action: str # "approve" | "reject" - reject_reason: str = "" + action: str = Field(..., max_length=30) # "approve" | "reject" + reject_reason: str = Field("", max_length=2000) @router.patch("/foto-submissions/{sub_id}") @@ -575,19 +575,19 @@ async def get_rasse_stats(slug: str, user=Depends(get_current_user_optional)): # Schemas für Interesse und Züchter # ------------------------------------------------------------------ class InteresseCreate(BaseModel): - typ: str # "hat" oder "will" + typ: str = Field(..., max_length=30) # "hat" oder "will" class ZuchterCreate(BaseModel): - rasse_slug: str - name: str - zwingername: str = "" - ort: str = "" - plz: str = "" - bundesland: str = "" + rasse_slug: str = Field(..., max_length=100) + name: str = Field(..., min_length=1, max_length=200) + zwingername: str = Field("", max_length=200) + ort: str = Field("", max_length=200) + plz: str = Field("", max_length=20) + bundesland: str = Field("", max_length=100) vdh_mitglied: int = 0 - website: str = "" - telefon: str = "" - beschreibung: str = "" + website: str = Field("", max_length=500) + telefon: str = Field("", max_length=30) + beschreibung: str = Field("", max_length=10000) # ------------------------------------------------------------------ diff --git a/backend/routes/zucht_hunde.py b/backend/routes/zucht_hunde.py index 8ef8c72..45aed56 100644 --- a/backend/routes/zucht_hunde.py +++ b/backend/routes/zucht_hunde.py @@ -2,7 +2,7 @@ import logging from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional from database import db @@ -134,108 +134,108 @@ def _ik_rating(ik: float) -> str: # Pydantic-Schemas # ------------------------------------------------------------------ class HundCreate(BaseModel): - name: str - rufname: Optional[str] = None - geschlecht: str # maennlich|weiblich - geburtsdatum: Optional[str] = None - sterbedatum: Optional[str] = None - chip_nr: Optional[str] = None - taetowiernummer: Optional[str] = None - zuchtbuchnummer: Optional[str] = None - farbe: Optional[str] = None + name: str = Field(..., min_length=1, max_length=200) + rufname: Optional[str] = Field(None, max_length=80) + geschlecht: str = Field(..., max_length=20) # maennlich|weiblich + geburtsdatum: Optional[str] = Field(None, max_length=32) + sterbedatum: Optional[str] = Field(None, max_length=32) + chip_nr: Optional[str] = Field(None, max_length=50) + taetowiernummer: Optional[str] = Field(None, max_length=50) + zuchtbuchnummer: Optional[str] = Field(None, max_length=100) + farbe: Optional[str] = Field(None, max_length=100) vater_id: Optional[int] = None mutter_id: Optional[int] = None - zuechter_name: Optional[str] = None - eigentuemer_name: Optional[str] = None + zuechter_name: Optional[str] = Field(None, max_length=200) + eigentuemer_name: Optional[str] = Field(None, max_length=200) is_public: int = 1 - notiz: Optional[str] = None - foto_url: Optional[str] = None + notiz: Optional[str] = Field(None, max_length=5000) + foto_url: Optional[str] = Field(None, max_length=500) class HundUpdate(BaseModel): - name: Optional[str] = None - rufname: Optional[str] = None - geschlecht: Optional[str] = None - geburtsdatum: Optional[str] = None - sterbedatum: Optional[str] = None - chip_nr: Optional[str] = None - taetowiernummer: Optional[str] = None - zuchtbuchnummer: Optional[str] = None - farbe: Optional[str] = None + name: Optional[str] = Field(None, max_length=200) + rufname: Optional[str] = Field(None, max_length=80) + geschlecht: Optional[str] = Field(None, max_length=20) + geburtsdatum: Optional[str] = Field(None, max_length=32) + sterbedatum: Optional[str] = Field(None, max_length=32) + chip_nr: Optional[str] = Field(None, max_length=50) + taetowiernummer: Optional[str] = Field(None, max_length=50) + zuchtbuchnummer: Optional[str] = Field(None, max_length=100) + farbe: Optional[str] = Field(None, max_length=100) vater_id: Optional[int] = None mutter_id: Optional[int] = None - zuechter_name: Optional[str] = None - eigentuemer_name: Optional[str] = None + zuechter_name: Optional[str] = Field(None, max_length=200) + eigentuemer_name: Optional[str] = Field(None, max_length=200) is_public: Optional[int] = None - notiz: Optional[str] = None - foto_url: Optional[str] = None + notiz: Optional[str] = Field(None, max_length=5000) + foto_url: Optional[str] = Field(None, max_length=500) class HealthTestCreate(BaseModel): - test_typ: str # HD|ED|OCD|augen|herz|patella|ZTP|custom - test_name: Optional[str] = None - ergebnis: Optional[str] = None - untersuch_am: Optional[str] = None - gueltig_bis: Optional[str] = None - untersucher: Optional[str] = None - labor: Optional[str] = None - zertifikat_nr: Optional[str] = None + test_typ: str = Field(..., max_length=50) # HD|ED|OCD|augen|herz|patella|ZTP|custom + test_name: Optional[str] = Field(None, max_length=200) + ergebnis: Optional[str] = Field(None, max_length=500) + untersuch_am: Optional[str] = Field(None, max_length=32) + gueltig_bis: Optional[str] = Field(None, max_length=32) + untersucher: Optional[str] = Field(None, max_length=200) + labor: Optional[str] = Field(None, max_length=200) + zertifikat_nr: Optional[str] = Field(None, max_length=100) is_public: int = 1 class HealthTestUpdate(BaseModel): - test_typ: Optional[str] = None - test_name: Optional[str] = None - ergebnis: Optional[str] = None - untersuch_am: Optional[str] = None - gueltig_bis: Optional[str] = None - untersucher: Optional[str] = None - labor: Optional[str] = None - zertifikat_nr: Optional[str] = None + test_typ: Optional[str] = Field(None, max_length=50) + test_name: Optional[str] = Field(None, max_length=200) + ergebnis: Optional[str] = Field(None, max_length=500) + untersuch_am: Optional[str] = Field(None, max_length=32) + gueltig_bis: Optional[str] = Field(None, max_length=32) + untersucher: Optional[str] = Field(None, max_length=200) + labor: Optional[str] = Field(None, max_length=200) + zertifikat_nr: Optional[str] = Field(None, max_length=100) is_public: Optional[int] = None class GeneticTestCreate(BaseModel): - marker_name: str # MDR1|PRA-prcd|DM|vWD|HUU etc. - marker_kategorie: Optional[str] = None # krankheit|farbe|eigenschaft - genotyp: Optional[str] = None # +/+|+/-|-/- - ergebnis_klasse: Optional[str] = None # clear|carrier|affected - getestet_am: Optional[str] = None - labor: Optional[str] = None - zertifikat_nr: Optional[str] = None + marker_name: str = Field(..., max_length=100) # MDR1|PRA-prcd|DM|vWD|HUU etc. + marker_kategorie: Optional[str] = Field(None, max_length=50) # krankheit|farbe|eigenschaft + genotyp: Optional[str] = Field(None, max_length=20) # +/+|+/-|-/- + ergebnis_klasse: Optional[str] = Field(None, max_length=50) # clear|carrier|affected + getestet_am: Optional[str] = Field(None, max_length=32) + labor: Optional[str] = Field(None, max_length=200) + zertifikat_nr: Optional[str] = Field(None, max_length=100) is_public: int = 1 class GeneticTestUpdate(BaseModel): - marker_name: Optional[str] = None - marker_kategorie: Optional[str] = None - genotyp: Optional[str] = None - ergebnis_klasse: Optional[str] = None - getestet_am: Optional[str] = None - labor: Optional[str] = None - zertifikat_nr: Optional[str] = None + marker_name: Optional[str] = Field(None, max_length=100) + marker_kategorie: Optional[str] = Field(None, max_length=50) + genotyp: Optional[str] = Field(None, max_length=20) + ergebnis_klasse: Optional[str] = Field(None, max_length=50) + getestet_am: Optional[str] = Field(None, max_length=32) + labor: Optional[str] = Field(None, max_length=200) + zertifikat_nr: Optional[str] = Field(None, max_length=100) is_public: Optional[int] = None class TitelCreate(BaseModel): - titel_typ: str # ausstellung|arbeit|sport|zucht|champion|custom - titel_name: str - verliehen_am: Optional[str] = None - ort: Optional[str] = None - richter: Optional[str] = None - ausstellung: Optional[str] = None - formwert: Optional[str] = None + titel_typ: str = Field(..., max_length=50) # ausstellung|arbeit|sport|zucht|champion|custom + titel_name: str = Field(..., min_length=1, max_length=200) + verliehen_am: Optional[str] = Field(None, max_length=32) + ort: Optional[str] = Field(None, max_length=200) + richter: Optional[str] = Field(None, max_length=200) + ausstellung: Optional[str] = Field(None, max_length=200) + formwert: Optional[str] = Field(None, max_length=100) is_public: int = 1 class TitelUpdate(BaseModel): - titel_typ: Optional[str] = None - titel_name: Optional[str] = None - verliehen_am: Optional[str] = None - ort: Optional[str] = None - richter: Optional[str] = None - ausstellung: Optional[str] = None - formwert: Optional[str] = None + titel_typ: Optional[str] = Field(None, max_length=50) + titel_name: Optional[str] = Field(None, max_length=200) + verliehen_am: Optional[str] = Field(None, max_length=32) + ort: Optional[str] = Field(None, max_length=200) + richter: Optional[str] = Field(None, max_length=200) + ausstellung: Optional[str] = Field(None, max_length=200) + formwert: Optional[str] = Field(None, max_length=100) is_public: Optional[int] = None diff --git a/backend/routes/zucht_ki.py b/backend/routes/zucht_ki.py index e25c49c..3f1fd43 100644 --- a/backend/routes/zucht_ki.py +++ b/backend/routes/zucht_ki.py @@ -3,7 +3,7 @@ import logging from datetime import date, timedelta from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, Literal from database import db @@ -41,7 +41,7 @@ class PaarungAnalyseBody(BaseModel): vater_id: int mutter_id: int ik_prozent: Optional[float] = None - welfare_level: Optional[str] = None + welfare_level: Optional[str] = Field(None, max_length=50) class HundBeschreibungBody(BaseModel): diff --git a/backend/static/css/design-system.css b/backend/static/css/design-system.css index d206049..4af159f 100644 --- a/backend/static/css/design-system.css +++ b/backend/static/css/design-system.css @@ -34,7 +34,7 @@ /* Text — Warmbraun aus dem Halsband */ --c-text: #2A1F14; --c-text-secondary: #7A6A58; - --c-text-muted: #B0A090; + --c-text-muted: #7F6B58; /* a11y: WCAG AA 4.74:1 auf --c-bg #FAF7F2 (vorher #B0A090 = 2.37:1) */ --c-text-inverse: #FAF7F2; /* Funktionsfarben */ @@ -179,7 +179,7 @@ --c-text: #F0EAE0; --c-text-secondary: #C0B0A0; - --c-text-muted: #806A58; + --c-text-muted: #A08878; /* a11y: WCAG AA 5.46:1 auf --c-bg #1A1410 (vorher #806A58 = 3.58:1) */ --c-text-inverse: #2A1F14; --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30); diff --git a/backend/static/css/layout.css b/backend/static/css/layout.css index ac91631..a54eedc 100644 --- a/backend/static/css/layout.css +++ b/backend/static/css/layout.css @@ -86,8 +86,8 @@ display: flex; align-items: center; justify-content: center; - width: 40px; - height: 40px; + width: 44px; + height: 44px; border-radius: var(--radius-md); color: var(--c-text-secondary); cursor: pointer; @@ -99,8 +99,8 @@ /* Hamburger-Button (nur Mobile) */ .header-menu-btn { - width: 40px; - height: 40px; + width: 44px; + height: 44px; display: flex; align-items: center; justify-content: center; diff --git a/backend/static/index.html b/backend/static/index.html index 8d4f016..b1e995d 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@