diff --git a/backend/database.py b/backend/database.py index 621ec5f..2dd12cd 100644 --- a/backend/database.py +++ b/backend/database.py @@ -834,3 +834,17 @@ def _migrate(conn_factory): CREATE INDEX IF NOT EXISTS idx_walk_inv_user ON walk_invitations(user_id, status); """) logger.info("Migration: walk_invitations Tabelle bereit.") + + # Referral-Code für jeden User (einmalig generiert) + try: + conn.execute("ALTER TABLE users ADD COLUMN referral_code TEXT UNIQUE") + conn.execute("ALTER TABLE users ADD COLUMN referred_by INTEGER REFERENCES users(id)") + # Bestehende User bekommen einen Code + import secrets, string + rows = conn.execute("SELECT id FROM users WHERE referral_code IS NULL").fetchall() + for r in rows: + code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8)) + conn.execute("UPDATE users SET referral_code=? WHERE id=?", (code, r['id'])) + logger.info("Migration: referral_code + referred_by zu users hinzugefügt.") + except Exception: + pass diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 501f06a..46169a5 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -1,5 +1,10 @@ """BAN YARO — Auth Routes""" +import os +import secrets +import string +from typing import Optional + from fastapi import APIRouter, HTTPException, Response, Depends from pydantic import BaseModel, EmailStr from database import db @@ -20,6 +25,12 @@ class RegisterRequest(BaseModel): email: EmailStr password: str name: str + ref_code: Optional[str] = None + + +def _gen_referral_code() -> str: + alphabet = string.ascii_uppercase + string.digits + return ''.join(secrets.choice(alphabet) for _ in range(8)) def _set_cookie(response: Response, token: str): @@ -45,10 +56,11 @@ async def register(data: RegisterRequest, response: Response): "SELECT 1 FROM users WHERE name=? COLLATE NOCASE", (name,) ).fetchone(): raise HTTPException(400, "Dieser Name ist bereits vergeben. Bitte wähle einen anderen.") + code = _gen_referral_code() try: conn.execute( - "INSERT INTO users (email, pw_hash, name) VALUES (?,?,?)", - (data.email, hash_password(data.password), name) + "INSERT INTO users (email, pw_hash, name, referral_code) VALUES (?,?,?,?)", + (data.email, hash_password(data.password), name, code) ) except Exception: # Fallback falls UNIQUE-Index greift (Race Condition) @@ -56,6 +68,16 @@ async def register(data: RegisterRequest, response: Response): user = conn.execute( "SELECT id, rolle FROM users WHERE email=?", (data.email,) ).fetchone() + new_user_id = user["id"] + + if data.ref_code: + referrer = conn.execute( + "SELECT id FROM users WHERE referral_code=? AND id != ?", + (data.ref_code.strip().upper(), new_user_id) + ).fetchone() + if referrer: + conn.execute("UPDATE users SET referred_by=? WHERE id=?", + (referrer['id'], new_user_id)) token = create_token(user["id"], user["rolle"]) _set_cookie(response, token) @@ -90,6 +112,21 @@ async def logout(response: Response): return {"ok": True} +@router.get("/referral") +async def get_referral_info(user=Depends(get_current_user)): + with db() as conn: + row = conn.execute( + "SELECT referral_code, (SELECT COUNT(*) FROM users WHERE referred_by=?) AS count FROM users WHERE id=?", + (user['id'], user['id']) + ).fetchone() + base = os.getenv("APP_URL", "https://banyaro.app") + return { + "code": row["referral_code"], + "count": row["count"], + "link": f"{base}/?ref={row['referral_code']}", + } + + @router.get("/me") async def me(user=Depends(get_current_user)): with db() as conn: diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 2065553..9a362a5 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -78,8 +78,10 @@ const API = (() => { login(email, password) { return post('/auth/login', { email, password }); }, - register(email, password, name) { - return post('/auth/register', { email, password, name }); + register(email, password, name, ref_code) { + const body = { email, password, name }; + if (ref_code) body.ref_code = ref_code; + return post('/auth/register', body); }, logout() { localStorage.removeItem('by_token'); @@ -88,6 +90,7 @@ const API = (() => { me() { return get('/auth/me'); }, + referral: () => get('/auth/referral'), }; // ---------------------------------------------------------- diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 8e46bc5..7df2a7c 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 = '221'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '222'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { @@ -687,6 +687,15 @@ const App = (() => { // INITIALISIERUNG // ---------------------------------------------------------- async function init() { + // Referral-Code aus URL ?ref=CODE speichern + const urlParams = new URLSearchParams(window.location.search); + const refCode = urlParams.get('ref'); + if (refCode) { + sessionStorage.setItem('by_ref_code', refCode.toUpperCase()); + // URL bereinigen ohne Reload + history.replaceState({}, '', window.location.pathname + window.location.hash); + } + _bindNavigation(); await _checkAuth(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 6aeb9d9..1e82b46 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -227,6 +227,17 @@ window.Page_settings = (() => { + +
Nicht verfügbar.
'; } } // ---------------------------------------------------------- @@ -662,8 +711,10 @@ window.Page_settings = (() => { } await UI.asyncButton(btn, async () => { - const result = await API.auth.register(fd.email, fd.password, fd.name.trim()); + const refCode = sessionStorage.getItem('by_ref_code') || ''; + const result = await API.auth.register(fd.email, fd.password, fd.name.trim(), refCode || undefined); localStorage.setItem('by_token', result.token); + if (refCode) sessionStorage.removeItem('by_ref_code'); _appState.user = await API.auth.me(); document.getElementById('sidebar-username').textContent = _appState.user.name; diff --git a/backend/static/sw.js b/backend/static/sw.js index 38c9efe..ff928ef 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-v242'; +const CACHE_VERSION = 'by-v244'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten