diff --git a/backend/database.py b/backend/database.py index e428a56..9822d42 100644 --- a/backend/database.py +++ b/backend/database.py @@ -560,9 +560,10 @@ def _migrate(conn_factory): ("users", "ki_zucht_beschreibung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_jahresbericht", "INTEGER NOT NULL DEFAULT 1"), # Partner-Code + Gründer-Lizenz - ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"), - ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"), - ("users", "founder_number", "INTEGER"), + ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"), + ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"), + ("users", "founder_number", "INTEGER"), + ("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"), ] with conn_factory() as conn: for table, column, col_type in migrations: diff --git a/backend/routes/auth.py b/backend/routes/auth.py index e46cda0..fb30584 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -97,9 +97,8 @@ async def register(data: RegisterRequest, response: Response, request: Request): "SELECT COUNT(*) FROM users WHERE is_founder=1" ).fetchone()[0] if total_founders < 100: - founder_num = total_founders + 1 - updates["is_founder"] = 1 - updates["founder_number"] = founder_num + # Pending — wird nach erstem Hunde-Profil mit Plausibilitätsprüfung aktiviert + updates["is_founder_pending"] = 1 set_clause = ", ".join(f"{k}=?" for k in updates) conn.execute( f"UPDATE users SET {set_clause} WHERE id=?", @@ -198,7 +197,7 @@ async def me(user=Depends(get_current_user)): """SELECT id, name, real_name, email, rolle, is_premium, email_verified, bio, wohnort, erfahrung, social_link, profil_sichtbarkeit, avatar_url, created_at, - is_founder, is_partner, founder_number + is_founder, is_partner, founder_number, is_founder_pending FROM users WHERE id=?""", (user["id"],) ).fetchone() diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 8e176f8..74f1c95 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -78,6 +78,41 @@ async def list_dogs(user=Depends(get_current_user)): return result +def _is_plausible_dog(name: str, rasse: str, geburtstag) -> tuple[bool, str]: + """Einfache Plausibilitätsprüfung für Hunde-Profile.""" + import re, datetime + name = (name or "").strip() + rasse = (rasse or "").strip() + + if len(name) < 2: + return False, "Der Name muss mindestens 2 Zeichen haben." + if not re.search(r'[a-zA-ZäöüÄÖÜß]', name): + return False, "Der Name muss mindestens einen Buchstaben enthalten." + if len(set(name.lower())) < 2: + return False, "Bitte einen echten Namen eingeben." + + if rasse and len(rasse) < 2: + return False, "Bitte eine gültige Rasse eingeben." + if rasse and not re.search(r'[a-zA-ZäöüÄÖÜß]', rasse): + return False, "Die Rasse muss Buchstaben enthalten." + + if geburtstag: + try: + if isinstance(geburtstag, str): + year = int(geburtstag[:4]) + else: + year = geburtstag.year + now = datetime.date.today().year + if year > now: + return False, "Das Geburtsdatum liegt in der Zukunft." + if year < now - 30: + return False, "Das Geburtsdatum ist unrealistisch." + except Exception: + pass + + return True, "" + + @router.post("") async def create_dog(data: DogCreate, user=Depends(get_current_user)): with db() as conn: @@ -93,6 +128,28 @@ async def create_dog(data: DogCreate, user=Depends(get_current_user)): "SELECT * FROM dogs WHERE user_id=? ORDER BY id DESC LIMIT 1", (user["id"],) ).fetchone() + + # Gründer-Aktivierung: erstes Hunde-Profil + is_founder_pending + user_row = conn.execute( + "SELECT is_founder_pending, is_founder FROM users WHERE id=?", + (user["id"],) + ).fetchone() + if user_row and user_row["is_founder_pending"] and not user_row["is_founder"]: + dog_count = conn.execute( + "SELECT COUNT(*) FROM dogs WHERE user_id=?", (user["id"],) + ).fetchone()[0] + if dog_count == 1: # genau dieser erste Hund + plausible, reason = _is_plausible_dog(data.name, data.rasse, data.geburtstag) + if plausible: + total = conn.execute( + "SELECT COUNT(*) FROM users WHERE is_founder=1" + ).fetchone()[0] + if total < 100: + conn.execute( + "UPDATE users SET is_founder=1, founder_number=?, is_founder_pending=0 WHERE id=?", + (total + 1, user["id"]) + ) + return dict(dog) diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 692526b..3680a86 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '542'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '543'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.1.4'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 9a6b596..8f45cf6 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -149,6 +149,12 @@ window.Page_settings = (() => { ? ` ${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'} + ` + : u.is_founder_pending + ? ` + + Gründer-Platz reserviert ` : ''} ${u.is_partner ? ` @@ -1525,7 +1531,9 @@ window.Page_settings = (() => { _appState.activeDog = null; document.getElementById('header-login-btn')?.remove(); - const greeting = _appState.user.is_founder + const greeting = _appState.user.is_founder_pending + ? `Willkommen, ${_appState.user.name}! 🎉 Dein Gründer-Platz ist reserviert — leg jetzt dein Hunde-Profil an um ihn zu sichern!` + : _appState.user.is_founder ? `Willkommen, Gründer ${_appState.user.name}! 🎉` : `Willkommen bei Ban Yaro, ${_appState.user.name}!`; UI.toast.success(greeting); diff --git a/backend/static/sw.js b/backend/static/sw.js index d493e2a..54f2cc0 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v565'; +const CACHE_VERSION = 'by-v566'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache