Feature: Referral-System — User wirbt User
- DB: referral_code (8-stellig, eindeutig) + referred_by zu users Bestehende User erhalten automatisch einen Code - GET /api/auth/referral: Code, Link und Anzahl geworbener User - POST /api/auth/register: ref_code Parameter für Zuordnung - Settings: 'App empfehlen'-Karte mit Link, Teilen-Button und Botschafter-Badges (Botschafter ab 1, Super ab 5, Top ab 10 Einladungen) - app.js: ?ref=CODE aus URL in sessionStorage speichern - APP_VER 222, SW by-v244
This commit is contained in:
parent
82d9e26823
commit
6d757b86c2
6 changed files with 121 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,17 @@ window.Page_settings = (() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App empfehlen -->
|
||||
<div class="card" style="margin-bottom:var(--space-5)" id="referral-card">
|
||||
<div style="padding:var(--space-4);border-bottom:1px solid var(--c-border)">
|
||||
<div style="font-weight:600;margin-bottom:2px">${UI.icon('arrow-square-out')} App empfehlen</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
Lade Freunde ein — jede erfolgreiche Einladung wird in deinem Profil angezeigt.
|
||||
</div>
|
||||
</div>
|
||||
<div id="referral-body" style="padding:var(--space-4)">Lade…</div>
|
||||
</div>
|
||||
|
||||
<!-- App installieren -->
|
||||
<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);
|
||||
|
|
@ -470,6 +481,44 @@ window.Page_settings = (() => {
|
|||
? 'Pocket-Modus aktiviert — Bildschirm bleibt bei Aufzeichnung an.'
|
||||
: 'Pocket-Modus deaktiviert.');
|
||||
});
|
||||
|
||||
_loadReferral();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// REFERRAL — Einladungslink laden und rendern
|
||||
// ----------------------------------------------------------
|
||||
async function _loadReferral() {
|
||||
const el = document.getElementById('referral-body');
|
||||
if (!el) return;
|
||||
try {
|
||||
const r = await API.auth.referral();
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<div style="flex:1;background:var(--c-surface-2);border-radius:var(--radius-md);
|
||||
padding:var(--space-2) var(--space-3);font-family:monospace;font-size:var(--text-sm);
|
||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${r.link}</div>
|
||||
<button class="btn btn-primary btn-sm" id="ref-share-btn">${UI.icon('arrow-square-out')} Teilen</button>
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
${UI.icon('users')} <strong>${r.count}</strong> ${r.count === 1 ? 'Person' : 'Personen'} über deinen Link registriert
|
||||
</div>
|
||||
${r.count > 0 ? `
|
||||
<div style="margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||
${r.count >= 1 ? `<span class="badge badge-primary">${UI.icon('star')} Botschafter</span>` : ''}
|
||||
${r.count >= 5 ? `<span class="badge badge-primary">${UI.icon('star')} Super-Botschafter</span>` : ''}
|
||||
${r.count >= 10 ? `<span class="badge badge-primary">${UI.icon('star')} Top-Botschafter</span>` : ''}
|
||||
</div>` : ''}
|
||||
`;
|
||||
document.getElementById('ref-share-btn')?.addEventListener('click', async () => {
|
||||
if (navigator.share) {
|
||||
navigator.share({ title: 'Ban Yaro — Die Hunde-App', text: 'Schau dir Ban Yaro an!', url: r.link }).catch(() => {});
|
||||
} else {
|
||||
await navigator.clipboard.writeText(r.link);
|
||||
UI.toast.success('Link kopiert!');
|
||||
}
|
||||
});
|
||||
} catch { el.innerHTML = '<p style="color:var(--c-text-muted)">Nicht verfügbar.</p>'; }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue