commit 4cca794b0a93e49f4037ec03507cc59e5cf3ca7f Author: root Date: Sun Jun 28 16:51:38 2026 +0000 Initial commit diff --git a/.env b/.env new file mode 100644 index 0000000..9c7633f --- /dev/null +++ b/.env @@ -0,0 +1,16 @@ +# Externe MySQL-Datenbank +DB_HOST=10.10.10.155 +DB_PORT=3306 +DB_USER=tu_skyevouchers +DB_PASSWORD=5AkYQusc[ju3*05L +DB_NAME=skyevouchers + +# Admin-Bereich +ADMIN_PASSWORD=uIVa8yJe01w56gLY4JkkBqj5*lt7UL&z + +# Flask Session-Schlüssel (langen zufälligen String verwenden) +SECRET_KEY=r@B33sY6jya0u*UcqZ$*0Db9PZORNcHJ + +# Gotify-Benachrichtigungen +GOTIFY_URL=https://push.re.dfined.net +GOTIFY_TOKEN=AiZmp5jTp0pip70 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9c7633f --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Externe MySQL-Datenbank +DB_HOST=10.10.10.155 +DB_PORT=3306 +DB_USER=tu_skyevouchers +DB_PASSWORD=5AkYQusc[ju3*05L +DB_NAME=skyevouchers + +# Admin-Bereich +ADMIN_PASSWORD=uIVa8yJe01w56gLY4JkkBqj5*lt7UL&z + +# Flask Session-Schlüssel (langen zufälligen String verwenden) +SECRET_KEY=r@B33sY6jya0u*UcqZ$*0Db9PZORNcHJ + +# Gotify-Benachrichtigungen +GOTIFY_URL=https://push.re.dfined.net +GOTIFY_TOKEN=AiZmp5jTp0pip70 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aada888 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 5000 +CMD ["python", "app.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..273a65a --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# skyevouchers + +Web-App zur Ausgabe von UniFi Hotspot-Vouchers mit Admin-Backend. + +## Features +- **Frontend**: Zeigt einen Voucher-Code mit Slot-Maschinen-Animation an +- **Einmalig**: Jeder Code kann nur einmal verwendet werden +- **Admin** (`/admin`): Passwort-geschütztes Dashboard + - Voucher-Import per Paste aus UniFi-Export + - Übersicht verfügbarer & verwendeter Vouchers +- **Gotify-Benachrichtigung** wenn ≤ 2 Vouchers übrig sind +- MySQL-Datenbank + +## Schnellstart mit Docker + +```bash +# 1. Repo klonen / Dateien kopieren +# 2. .env erstellen +cp .env.example .env +# .env anpassen (Passwörter, Gotify-URL etc.) + +# 3. Starten +docker compose up -d +``` + +Die App läuft dann auf `http://localhost:5000` +Admin: `http://localhost:5000/admin` + +## Ohne Docker (lokal) + +```bash +pip install -r requirements.txt + +# Umgebungsvariablen setzen (oder .env manuell sourcen) +export DB_HOST=localhost +export DB_USER=root +export DB_PASSWORD=... +export DB_NAME=skyevouchers +export ADMIN_PASSWORD=geheim +export GOTIFY_URL=http://gotify:8080 +export GOTIFY_TOKEN=abc123 + +python app.py +``` + +## Vouchers importieren + +1. In UniFi → Vouchers exportieren (als Text kopieren) +2. Admin-Bereich öffnen (`/admin`) +3. Text in das Import-Feld einfügen → „Vouchers extrahieren & importieren" + +Das Format wird automatisch erkannt: `GUESTS64071-58673 1 day ...` +Extrahiert werden Codes im Format `XXXXX-XXXXX`. + +## Umgebungsvariablen + +| Variable | Standard | Beschreibung | +|-----------------|---------------------|---------------------------| +| `DB_HOST` | `localhost` | MySQL-Host | +| `DB_USER` | `root` | MySQL-Benutzer | +| `DB_PASSWORD` | *(leer)* | MySQL-Passwort | +| `DB_NAME` | `skyevouchers` | Datenbankname | +| `ADMIN_PASSWORD`| `admin123` | Admin-Login-Passwort | +| `SECRET_KEY` | *(default)* | Flask Session Secret | +| `GOTIFY_URL` | `http://localhost:8080` | Gotify-Basis-URL | +| `GOTIFY_TOKEN` | *(leer)* | Gotify App-Token | diff --git a/app.py b/app.py new file mode 100644 index 0000000..f0db6bc --- /dev/null +++ b/app.py @@ -0,0 +1,186 @@ +from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash +import mysql.connector +import os +import re +import requests +import random +import string +from datetime import datetime +from functools import wraps + +app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', 'skyevouchers-secret-key-2024') + +# Config +DB_CONFIG = { + 'host': os.environ.get('DB_HOST', 'localhost'), + 'port': int(os.environ.get('DB_PORT', 3306)), + 'user': os.environ.get('DB_USER', 'root'), + 'password': os.environ.get('DB_PASSWORD', ''), + 'database': os.environ.get('DB_NAME', 'skyevouchers'), +} +ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'admin123') +GOTIFY_URL = os.environ.get('GOTIFY_URL', 'http://localhost:8080') +GOTIFY_TOKEN = os.environ.get('GOTIFY_TOKEN', '') +LOW_VOUCHER_THRESHOLD = 2 + + +def get_db(): + return mysql.connector.connect(**DB_CONFIG) + + +def init_db(): + conn = get_db() + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS vouchers ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(20) NOT NULL UNIQUE, + used BOOLEAN DEFAULT FALSE, + used_at DATETIME NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )''') + conn.commit() + conn.close() + + +def send_gotify_notification(remaining): + if not GOTIFY_TOKEN: + return + try: + requests.post( + f"{GOTIFY_URL}/message", + json={ + "title": "⚠️ Skye Vouchers – Niedriger Bestand", + "message": f"Nur noch {remaining} Voucher(s) verfügbar! Bitte neue Vouchers hinzufügen.", + "priority": 8 + }, + params={"token": GOTIFY_TOKEN}, + timeout=5 + ) + except Exception: + pass + + +def login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get('admin_logged_in'): + return redirect(url_for('admin_login')) + return f(*args, **kwargs) + return decorated + + +# ── Frontend ────────────────────────────────────────────────────────────────── + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/api/get-voucher', methods=['POST']) +def get_voucher(): + conn = get_db() + c = conn.cursor(dictionary=True) + c.execute('SELECT id, code FROM vouchers WHERE used = FALSE ORDER BY RAND() LIMIT 1') + voucher = c.fetchone() + if not voucher: + conn.close() + return jsonify({'error': 'Keine Vouchers mehr verfügbar'}), 404 + + c.execute('UPDATE vouchers SET used = TRUE, used_at = %s WHERE id = %s', + (datetime.now(), voucher['id'])) + conn.commit() + + # Check remaining + c.execute('SELECT COUNT(*) AS cnt FROM vouchers WHERE used = FALSE') + remaining = c.fetchone()['cnt'] + conn.close() + + if remaining <= LOW_VOUCHER_THRESHOLD: + send_gotify_notification(remaining) + + return jsonify({'code': voucher['code']}) + + +# ── Admin ───────────────────────────────────────────────────────────────────── + +@app.route('/admin/login', methods=['GET', 'POST']) +def admin_login(): + if request.method == 'POST': + if request.form.get('password') == ADMIN_PASSWORD: + session['admin_logged_in'] = True + return redirect(url_for('admin_dashboard')) + flash('Falsches Passwort', 'error') + return render_template('admin_login.html') + + +@app.route('/admin/logout') +def admin_logout(): + session.clear() + return redirect(url_for('admin_login')) + + +@app.route('/admin') +@login_required +def admin_dashboard(): + conn = get_db() + c = conn.cursor(dictionary=True) + c.execute('SELECT COUNT(*) AS cnt FROM vouchers WHERE used = FALSE') + available = c.fetchone()['cnt'] + c.execute('SELECT COUNT(*) AS cnt FROM vouchers WHERE used = TRUE') + used_count = c.fetchone()['cnt'] + c.execute('SELECT COUNT(*) AS cnt FROM vouchers') + total = c.fetchone()['cnt'] + c.execute('SELECT code, used_at FROM vouchers WHERE used = TRUE ORDER BY used_at DESC LIMIT 20') + recent_used = c.fetchall() + c.execute('SELECT code, created_at FROM vouchers WHERE used = FALSE ORDER BY created_at DESC LIMIT 20') + available_vouchers = c.fetchall() + conn.close() + return render_template('admin_dashboard.html', + available=available, + used_count=used_count, + total=total, + recent_used=recent_used, + available_vouchers=available_vouchers) + + +@app.route('/admin/import', methods=['POST']) +@login_required +def admin_import(): + text = request.form.get('voucher_text', '') + # Extract codes matching xxxxx-xxxxx (5 digits, dash, 5 digits) + codes = re.findall(r'\b(\d{5}-\d{5})\b', text) + if not codes: + flash('Keine gültigen Voucher-Codes gefunden.', 'error') + return redirect(url_for('admin_dashboard')) + + conn = get_db() + c = conn.cursor() + imported = 0 + skipped = 0 + for code in codes: + try: + c.execute('INSERT INTO vouchers (code) VALUES (%s)', (code,)) + imported += 1 + except mysql.connector.IntegrityError: + skipped += 1 + conn.commit() + conn.close() + flash(f'{imported} Voucher(s) importiert, {skipped} bereits vorhanden.', 'success') + return redirect(url_for('admin_dashboard')) + + +@app.route('/admin/delete/', methods=['POST']) +@login_required +def admin_delete(vid): + conn = get_db() + c = conn.cursor() + c.execute('DELETE FROM vouchers WHERE id = %s', (vid,)) + conn.commit() + conn.close() + return jsonify({'ok': True}) + + +if __name__ == '__main__': + init_db() + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0aefc81 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + web: + build: . + restart: always + ports: + - "8088:5000" + environment: + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT:-3306} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123} + GOTIFY_URL: ${GOTIFY_URL:-http://localhost:8080} + GOTIFY_TOKEN: ${GOTIFY_TOKEN:-} + SECRET_KEY: ${SECRET_KEY:-change-me-in-production} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..47c7856 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask>=3.0.0 +mysql-connector-python>=8.3.0 +requests>=2.31.0 diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html new file mode 100644 index 0000000..8d096c8 --- /dev/null +++ b/templates/admin_dashboard.html @@ -0,0 +1,241 @@ + + + + + +Admin Dashboard · skyevouchers + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% for cat, msg in messages %} +
{{ msg }}
+ {% endfor %} + {% endwith %} + + +
+
+
Verfügbar
+
{{ available }}
+
+
+
Verwendet
+
{{ used_count }}
+
+
+
Gesamt
+
{{ total }}
+
+
+ + +
+
+
Vouchers importieren
+
+
+
+ + +
+
+
+ + +
+ +
+
+
Verfügbare Vouchers
+
+
+ {% if available_vouchers %} + + + + {% for v in available_vouchers %} + + + + + + {% endfor %} + +
CodeErstellt
{{ v.code }}{{ v.created_at.strftime('%d.%m.%Y %H:%M') if v.created_at else '—' }}
+ {% else %} +
Keine verfügbaren Vouchers
+ {% endif %} +
+
+ + +
+
+
Zuletzt verwendet
+
+
+ {% if recent_used %} + + + + {% for v in recent_used %} + + + + + {% endfor %} + +
CodeVerwendet am
{{ v.code }}{{ v.used_at.strftime('%d.%m.%Y %H:%M') if v.used_at else '—' }}
+ {% else %} +
Noch keine Vouchers verwendet
+ {% endif %} +
+
+
+
+ + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..82cbd3e --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,82 @@ + + + + + +Admin · skyevouchers + + + +
+ +

Admin-Bereich

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% for cat, msg in messages %} +
{{ msg }}
+ {% endfor %} + {% endwith %} +
+ + + +
+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a309b45 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,383 @@ + + + + + +skyevouchers + + + +
+
skyevouchers.
+ +
+
WLAN-Zugangscode
+ +
+
+ — — — — — + +
+
+ +
+ +
+ + +
+
+ + +
+ + + +