"""BAN YARO — Züchter-Verwaltung (Antrag, Admin-Prüfung)""" import os import logging from datetime import datetime from zoneinfo import ZoneInfo from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user, require_premium from mailer import send_email, email_html router = APIRouter() logger = logging.getLogger(__name__) _TZ = ZoneInfo("Europe/Berlin") BREEDER_DOCS_DIR = os.getenv("BREEDER_DOCS_DIR", "/data/breeder_docs") os.makedirs(BREEDER_DOCS_DIR, exist_ok=True) ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "admin@banyaro.app") APP_URL = os.getenv("APP_URL", "https://banyaro.app") # ------------------------------------------------------------------ # Dependency: nur verifizierte Züchter + Admins # ------------------------------------------------------------------ def require_breeder(user=Depends(get_current_user)): if user["rolle"] not in ("breeder", "admin"): raise HTTPException(403, "Nur für verifizierte Züchter.") return user def require_admin(user=Depends(get_current_user)): if user["rolle"] != "admin": raise HTTPException(403, "Nur für Admins.") return user # ------------------------------------------------------------------ # GET /api/breeder/status — eigener Antragsstatus # ------------------------------------------------------------------ @router.get("/breeder/status") async def breeder_status(user=Depends(get_current_user)): with db() as conn: row = conn.execute( "SELECT rolle, breeder_status FROM users WHERE id=?", (user["id"],) ).fetchone() if not row: raise HTTPException(404, "User nicht gefunden.") profile = None if row["rolle"] in ("breeder", "admin"): profile = conn.execute( "SELECT id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung, verified_at " "FROM breeder_profiles WHERE user_id=?", (user["id"],) ).fetchone() return { "rolle": row["rolle"], "breeder_status": row["breeder_status"], "profile": dict(profile) if profile else None, } # ------------------------------------------------------------------ # POST /api/breeder/apply — Antrag stellen # ------------------------------------------------------------------ @router.post("/breeder/apply") async def breeder_apply( zwingername: str = Form(...), rasse_text: str = Form(...), verein: str = Form(...), vdh_mitglied: int = Form(0), stadt: str = Form(...), website: str = Form(""), beschreibung: str = Form(""), dokument: UploadFile = File(...), user=Depends(get_current_user), ): with db() as conn: row = conn.execute( "SELECT rolle, breeder_status FROM users WHERE id=?", (user["id"],) ).fetchone() if not row: raise HTTPException(404, "User nicht gefunden.") if row["rolle"] == "breeder": raise HTTPException(400, "Du bist bereits verifizierter Züchter.") if row["breeder_status"] == "pending": raise HTTPException(400, "Du hast bereits einen offenen Antrag.") # Dokument validieren und speichern data = await dokument.read() if len(data) > 10 * 1024 * 1024: raise HTTPException(400, "Dokument zu groß (max. 10 MB).") ext = os.path.splitext(dokument.filename or "")[1].lower() if ext not in (".pdf", ".jpg", ".jpeg", ".png", ".webp"): raise HTTPException(400, "Nur PDF, JPG, PNG oder WebP erlaubt.") user_doc_dir = os.path.join(BREEDER_DOCS_DIR, str(user["id"])) os.makedirs(user_doc_dir, exist_ok=True) filename = f"antrag_{datetime.now(_TZ).strftime('%Y%m%d_%H%M%S')}{ext}" filepath = os.path.join(user_doc_dir, filename) with open(filepath, "wb") as f: f.write(data) with db() as conn: conn.execute( "UPDATE users SET breeder_status='pending' WHERE id=?", (user["id"],) ) # Profil-Entwurf anlegen (oder überschreiben wenn rejected) conn.execute( "INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung) " "VALUES (?,?,?,?,?,?,?,?) " "ON CONFLICT(user_id) DO UPDATE SET " "zwingername=excluded.zwingername, rasse_text=excluded.rasse_text, " "verein=excluded.verein, vdh_mitglied=excluded.vdh_mitglied, " "stadt=excluded.stadt, website=excluded.website, beschreibung=excluded.beschreibung, " "verified_at=NULL", (user["id"], zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung) ) conn.execute( "INSERT INTO breeder_documents (user_id, dokument_typ, file_path) VALUES (?,?,?)", (user["id"], "antrag", filepath) ) # Admin benachrichtigen admin_body = f"""
Neuer Züchter-Antrag eingegangen:
| Von | {user['name']} ({user['email']}) |
| Zwingername | {zwingername} |
| Rasse | {rasse_text} |
| Verein | {verein} |
| VDH | {'Ja' if vdh_mitglied else 'Nein'} |
| Stadt | {stadt} |
Hallo {user['name']},
dein Züchter-Profil bei Ban Yaro wurde erfolgreich verifiziert. 🎉
Ab sofort hast du Zugang zu allen Züchter-Features.
Hallo {user['name']},
leider konnten wir deinen Züchter-Antrag aktuell nicht bestätigen.
Du kannst jederzeit einen neuen Antrag stellen. Bei Fragen erreichst du uns unter {ADMIN_EMAIL}.
""" try: await send_email( user["email"], "Dein Züchter-Antrag bei Ban Yaro", email_html(reject_body), f"Hallo {user['name']}, dein Antrag wurde abgelehnt. Grund: {body.grund}", ) except Exception as e: logger.warning(f"Ablehnungs-Mail nicht gesendet: {e}") return {"message": f"Antrag von {user['name']} abgelehnt."} # ------------------------------------------------------------------ # GET /api/breeder/profil/{zwingername} — öffentliches Profil (angereichert) # ------------------------------------------------------------------ @router.get("/breeder/profil/{zwingername}") async def breeder_public_profile(zwingername: str): with db() as conn: row = conn.execute(""" SELECT bp.id, bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied, bp.stadt, bp.website, bp.beschreibung, bp.location_lat, bp.location_lng, bp.verified_at, bp.created_at, u.id AS zuechter_user_id, u.name AS zuechter_name FROM breeder_profiles bp JOIN users u ON u.id = bp.user_id WHERE LOWER(bp.zwingername) = LOWER(?) AND u.rolle IN ('breeder', 'admin') AND (u.breeder_status = 'approved' OR u.rolle = 'admin') """, (zwingername,)).fetchone() if not row: raise HTTPException(404, "Züchter nicht gefunden.") breeder_id = row["id"] result = dict(row) # Öffentliche Zuchthunde + ihre wichtigsten Gesundheitstests + Titel hunde_rows = conn.execute(""" SELECT id, name, rufname, geschlecht, geburtsdatum, farbe, zuchtbuchnummer, foto_url FROM zucht_hunde WHERE breeder_id=? AND is_public=1 AND (sterbedatum IS NULL OR sterbedatum='') ORDER BY geschlecht, name """, (breeder_id,)).fetchall() hunde = [] for h in hunde_rows: hund = dict(h) # Gesundheitstests (nur öffentliche, nur HD/ED/Augen/Herz) tests = conn.execute(""" SELECT test_typ, ergebnis, test_name, untersuch_am FROM dog_health_tests WHERE hund_id=? AND is_public=1 AND test_typ IN ('HD','ED','augen','herz','OCD','patella','ZTP') ORDER BY test_typ, untersuch_am DESC """, (h["id"],)).fetchall() seen = set() hund["health_tests"] = [] for t in tests: if t["test_typ"] not in seen: seen.add(t["test_typ"]) hund["health_tests"].append(dict(t)) # Gentests (nur öffentliche, Zusammenfassung) gentests = conn.execute(""" SELECT COUNT(*) as total, SUM(CASE WHEN ergebnis_klasse='clear' THEN 1 ELSE 0 END) as clear_cnt FROM dog_genetic_tests WHERE hund_id=? AND is_public=1 """, (h["id"],)).fetchone() hund["gentests_total"] = gentests["total"] or 0 hund["gentests_clear"] = gentests["clear_cnt"] or 0 # Auszeichnungen (nur Zucht/Champion) titles = conn.execute(""" SELECT titel_name FROM dog_titles WHERE hund_id=? AND titel_typ IN ('champion','zucht','ausstellung') ORDER BY verliehen_am DESC LIMIT 3 """, (h["id"],)).fetchall() hund["titel"] = [t["titel_name"] for t in titles] hunde.append(hund) result["hunde"] = hunde # Sichtbare Würfe wuerfe = conn.execute(""" SELECT id, vater_name, mutter_name, geburt_datum, erwartetes_datum, status, welpen_gesamt, welpen_verfuegbar, preis_spanne, beschreibung FROM litters WHERE breeder_id=? AND sichtbar=1 AND status != 'abgeschlossen' ORDER BY COALESCE(geburt_datum, erwartetes_datum) DESC """, (breeder_id,)).fetchall() result["wuerfe"] = [dict(w) for w in wuerfe] # Gesundheits-Statistik (aggregiert über alle öffentlichen Hunde) hd_stats = conn.execute(""" SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1) AND test_typ='HD' AND is_public=1 GROUP BY ergebnis """, (breeder_id,)).fetchall() result["hd_stats"] = [dict(r) for r in hd_stats] ed_stats = conn.execute(""" SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1) AND test_typ='ED' AND is_public=1 GROUP BY ergebnis """, (breeder_id,)).fetchall() result["ed_stats"] = [dict(r) for r in ed_stats] # Logo = primäres Bild der entity_type='breeder' Fotos logo = conn.execute(""" SELECT file_path FROM breeder_photos WHERE breeder_id=? AND entity_type='breeder' AND is_primary=1 LIMIT 1 """, (breeder_id,)).fetchone() if not logo: logo = conn.execute(""" SELECT file_path FROM breeder_photos WHERE breeder_id=? AND entity_type='breeder' ORDER BY sort_order, id LIMIT 1 """, (breeder_id,)).fetchone() result["logo_url"] = f"/api/media/{logo['file_path']}" if logo else None # Öffentliche Fotos für die Gallery (alle entity_type='breeder', max. 12) photos = conn.execute(""" SELECT file_path, thumbnail_path, caption, is_primary FROM breeder_photos WHERE breeder_id=? AND entity_type='breeder' AND visibility IN ('public','inquiry') ORDER BY is_primary DESC, sort_order, id LIMIT 12 """, (breeder_id,)).fetchall() result["fotos"] = [{ "url": f"/api/media/{p['file_path']}", "thumb": f"/api/media/{p['thumbnail_path']}" if p['thumbnail_path'] else f"/api/media/{p['file_path']}", "caption": p["caption"] or "", "primary": bool(p["is_primary"]), } for p in photos] return result # ------------------------------------------------------------------ # POST /api/admin/breeder/create-profile — Admin-Schnellprofil # ------------------------------------------------------------------ @router.post("/admin/breeder/create-profile") async def admin_create_profile(admin=Depends(require_admin)): with db() as conn: existing = conn.execute( "SELECT id FROM breeder_profiles WHERE user_id=?", (admin["id"],) ).fetchone() if existing: return {"message": "Profil existiert bereits.", "profile_id": existing["id"]} cur = conn.execute( "INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, stadt, verified_at) " "VALUES (?, ?, ?, ?, ?, datetime('now'))", (admin["id"], "Admin-Zwinger", "Alle Rassen", "Admin", "Überall") ) conn.execute( "UPDATE users SET breeder_status='approved' WHERE id=?", (admin["id"],) ) return {"message": "Admin-Züchterprofil angelegt.", "profile_id": cur.lastrowid} # ------------------------------------------------------------------ # PUT /api/breeder/profile — eigenes Profil bearbeiten # ------------------------------------------------------------------ class BreederProfileUpdate(BaseModel): zwingername: Optional[str] = None rasse_text: Optional[str] = None verein: Optional[str] = None vdh_mitglied: Optional[int] = None stadt: Optional[str] = None website: Optional[str] = None beschreibung: Optional[str] = None @router.put("/breeder/profile") async def update_breeder_profile(body: BreederProfileUpdate, user=Depends(require_breeder)): with db() as conn: profile = conn.execute( "SELECT id FROM breeder_profiles WHERE user_id=?", (user["id"],) ).fetchone() if not profile: raise HTTPException(404, "Kein Züchter-Profil vorhanden.") fields = {k: v for k, v in body.model_dump().items() if v is not None} if not fields: return {"message": "Keine Änderungen."} set_clause = ", ".join(f"{k}=?" for k in fields) conn.execute( f"UPDATE breeder_profiles SET {set_clause} WHERE id=?", (*fields.values(), profile["id"]) ) return {"message": "Profil aktualisiert."} # ------------------------------------------------------------------ # GET /api/breeder/map — alle Züchter für Karte # ------------------------------------------------------------------ @router.get("/breeder/map") async def breeder_map_markers(): with db() as conn: rows = conn.execute(""" SELECT bp.id, bp.zwingername, bp.rasse_text, bp.stadt, bp.location_lat, bp.location_lng FROM breeder_profiles bp JOIN users u ON u.id = bp.user_id WHERE bp.verified_at IS NOT NULL AND u.rolle = 'breeder' """).fetchall() return [dict(r) for r in rows]