commit a1334ed865deb4dbc9062344295e12e7091e36f6 Author: Jesko Anschütz Date: Sun May 4 23:44:49 2025 +0200 first commit diff --git a/.env-dist b/.env-dist new file mode 100644 index 0000000..6b329dc --- /dev/null +++ b/.env-dist @@ -0,0 +1,10 @@ +UNIFI_HOST = 'wlan.morz.de' +UNIFI_USERNAME = 'admin' +UNIFI_PASSWORD = 'yourPassword' +UNIFI_PORT = 443 +UNIFI_SSL_VERIFY = True +UNIFI_SITE_ID = 'yourSiteID' +WLAN_SSID = 'your WLAN-SSID' +WLAN_PASSWORD = 'your WLAN Passwort' +LOGO_FILE = /app/custom/logo.png +WIFI_ICON = /app/custom/wifi-icon.png \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..368ed78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +script/__pycache__/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7060a93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13-slim + +# Arbeitsverzeichnis im Container +WORKDIR /app + +# Abhängigkeiten installieren +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY script/* /app/script-dist/ +COPY entrypoint.sh /app/ +RUN chmod +x /app/entrypoint.sh + +CMD ["sh", "/app/entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..19561b4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +name: linuxmuster-voucher + +services: + linuxmuster-voucher: + build: + context: . + dockerfile: Dockerfile + image: git.az-it.net/az/linuxmuster-voucher:latest + container_name: linuxmuster-voucher + ports: + - 42425:42425 + volumes: + - ./script:/app/script + # - ./lmn-logo.png:/app/script/static/logo.png:ro + + working_dir: /app/script + environment: + - DEV=false + - PYTHONUNBUFFERED=1 + env_file: + - .env + #restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..a1ee0cc --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +APPSCRIPT="/app/script/app.py" +# App-Name ist der Name des Python-Skripts ohne die Endung .py +APPNAME=$(basename "$APPSCRIPT" .py) +APPDIR=$(dirname "$APPSCRIPT") + +if [ -f "/app/script/app.py" ]; then + echo "Das Verzeichnis $APPDIR existiert und enthält die Datei $APPSCRIPT --> führe das Skript aus." +else + echo "Das Verzeichnis $APPDIR existiert nicht oder die Datei $APPSCRIPT fehlt. --> kopiere die Dateien aus dem Verzeichnis /app/script-dist in das Verzeichnis $APPDIR." + mkdir -p $APPDIR + cp -r /app/script-dist/* $APPDIR +fi + +echo "Setze Arbeitsverzeichnis auf $APPDIR" +cd $APPDIR +echo "Inhalt des Verzeichnisses $APPDIR:" +ls -l $APPDIR + +echo DEV=$DEV +if [ "${DEV:-}" = "true" ]; then + echo "DEBUGGING ist auf true gesetzt. Halte Container am Laufen aber starte kein gunicorn" + while true; do + echo "Schlafe 10 Minuten..." + sleep 600 + done +else + APP_WEBPORT=${APP_WEBPORT:-42425} + APP_WORKERS=${APP_WORKERS:-4} + echo "APP_WEBPORT ist auf $APP_WEBPORT gesetzt." + echo "Starte Gunicorn mit $APP_WORKERS Worker auf Port $APP_WEBPORT." + while true; do + echo "Starte gunicorn ..." + PYTHONPATH=$APPDIR gunicorn -w $APP_WORKERS -b 0.0.0.0:$APP_WEBPORT app:$APPNAME + echo "Script wurde beendet, warte 60 Sekunden bis zum nächsten Start." + sleep 60 + done +fi \ No newline at end of file diff --git a/lmn-logo.png b/lmn-logo.png new file mode 100644 index 0000000..6fb4976 Binary files /dev/null and b/lmn-logo.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0605dc0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +flask +gunicorn +authlib +requests +pyunifi +pillow +qrcode diff --git a/script/app.py b/script/app.py new file mode 100644 index 0000000..8065646 --- /dev/null +++ b/script/app.py @@ -0,0 +1,157 @@ +from pyunifi.controller import Controller + +import os +from authlib.integrations.flask_client import OAuth +from flask import Flask, jsonify, request, render_template, redirect, url_for, session, send_from_directory +from werkzeug.middleware.proxy_fix import ProxyFix +import uuid +import json +import qrcode +import base64 +import io + + + + +def str_to_bool(value): + """ + Konvertiert einen String in einen booleschen Wert. + """ + return value.lower() in ('true', '1', 'yes') + +UNIFI_HOST = os.getenv('UNIFI_HOST', '') +UNIFI_USERNAME = os.getenv('UNIFI_USERNAME', 'admin') +UNIFI_PASSWORD = os.getenv('UNIFI_PASSWORD', '') +UNIFI_PORT = int(os.getenv('UNIFI_PORT', 8443)) +UNIFI_SSL_VERIFY = str_to_bool(os.getenv('UNIFI_SSL_VERIFY', 'True')) +UNIFI_SITE_ID = os.getenv('UNIFI_SITE_ID', '') +DEBUG = str_to_bool(os.getenv('DEBUG', 'False')) +LOGO_FILE = os.getenv('LOGO_FILE', '/static/logo.png') +WIFI_ICON = os.getenv('WIFI_ICON', '/static/wifi-icon.png') +if DEBUG: + print("DEBUG mode is enabled.") + print(f"UNIFI_HOST: {UNIFI_HOST}") + print(f"UNIFI_USERNAME: {UNIFI_USERNAME}") + print(f"UNIFI_PASSWORD: {UNIFI_PASSWORD}") + print(f"UNIFI_PORT: {UNIFI_PORT}") + print(f"UNIFI_SSL_VERIFY: {UNIFI_SSL_VERIFY}") + print(f"UNIFI_SITE_ID: {UNIFI_SITE_ID}") + +LOGLEVEL = os.getenv('LOGLEVEL', 'INFO') + +def log(message, level='INFO'): + """ + Log a message with the specified log level. + """ + if level == 'DEBUG' and not DEBUG: + return + if level == 'INFO' and LOGLEVEL not in ['DEBUG', 'INFO']: + return + if level == 'WARNING' and LOGLEVEL not in ['DEBUG', 'INFO', 'WARNING']: + return + if level == 'ERROR' and LOGLEVEL not in ['DEBUG', 'INFO', 'WARNING', 'ERROR']: + return + print(f"{level}: {message}") + + +def check_site_id(site_id): + """ + Check if the given site ID exists in the controller and assign the right one if not. + """ + log(f"Checking site ID: {site_id}", 'DEBUG') + c = Controller(UNIFI_HOST, UNIFI_USERNAME, UNIFI_PASSWORD, port=UNIFI_PORT, ssl_verify=UNIFI_SSL_VERIFY, site_id=UNIFI_SITE_ID) + sites = c.get_sites() + for site in sites: + if site['name'] == site_id: + return site_id + if len(sites) == 1: + print("\n" * 3 , "#" * 60) + print(f"Since there is only one site in your system I can set UNIFI_SITE_ID to {sites[0]['name']}") + print(f"To suppress this message, set environment-variable UNIFI_SITE_ID to {sites[0]['name']}") + print("#" * 60, "\n" * 3) + + return sites[0]['name'] + else: + print("\n" * 3, "#" * 60) + print(f"Multiple sites found. Please specify a valid site ID.") + print(f"Available site IDs: {', '.join([site['name'] for site in sites])}") + print(f"Setting UNIFI_SITE_ID to {sites[0]['name']}") + print(f"To suppress this message, set environment-variable UNIFI_SITE_ID to the desired site ID") + print("#" * 60, "\n" * 3) + return sites[0]['name'] + +def get_voucher_list(controller): + """ + Get the list of vouchers from the controller. + """ + vouchers = controller.list_vouchers() + return vouchers + +def api_create_voucher(c, note="TEST", time=45, up_bandwidth=5000, down_bandwidth=10000, byte_quota=0): + """ + Create a voucher with the specified name and number of vouchers. + create_voucher(Anzahl Voucher, Anzahl Nutzer (0=ohne Limit), Zeit in Minuten, Upstream Bandwidth, Downstream Bandwidth, Byte Quota, Notiz) + """ + + voucher = c.create_voucher(1, 0, time, up_bandwidth=up_bandwidth, down_bandwidth=down_bandwidth, byte_quota=byte_quota, note=note) + code = voucher[0].get('code') + + log(f"Created voucher with code: {code}", 'INFO') + return code + + +# QR-Code für das WLAN generieren: +ssid = os.getenv('WLAN_SSID', 'TANNE') +password = os.getenv('WLAN_WPA2', 'hdndwlanar') +qr_data = f"WIFI:T:WPA;S:{ssid};P:{password};;" +qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=0, + ) +qr.add_data(qr_data) +qr.make(fit=True) +img = qr.make_image(fill_color="#000000", back_color="transparent") +buffer = io.BytesIO() +img.save(buffer, format="PNG") +buffer.seek(0) + + + + + +app = Flask(__name__) +# b64encode-Filter registrieren +@app.template_filter('b64encode') +def b64encode_filter(data): + """ + Base64-Encode für Jinja2-Templates. + """ + if isinstance(data, bytes): + return base64.b64encode(data).decode('utf-8') + elif hasattr(data, 'getvalue'): # Prüfen, ob es ein BytesIO-Objekt ist + return base64.b64encode(data.getvalue()).decode('utf-8') + return base64.b64encode(data.encode('utf-8')).decode('utf-8') + + +@app.route("/") +def home(): + return render_template("index.html", logo=LOGO_FILE) + +@app.route("/voucher/") +def create_voucher(minutes): + try: + c = Controller(UNIFI_HOST, UNIFI_USERNAME, UNIFI_PASSWORD, port=UNIFI_PORT, ssl_verify=UNIFI_SSL_VERIFY, site_id=check_site_id(UNIFI_SITE_ID)) + code = api_create_voucher(c, time=minutes) # Platzhalter für echten Code + # code aufbereiten: 5 Zeichen Bindestrich, 5 Zeichen Bindestrich, 5 Zeichen Bindestrich, 5 Zeichen + code = f"{code[:5]}-{code[5:10]}" + except Exception as e: + log(f"Error creating voucher: {e}", 'ERROR') + return jsonify({"error": str(e)}), 500 + return render_template("voucher.html", code=code, minutes=minutes, qr_code=buffer, wifi_icon=WIFI_ICON) + + +# Main script +if __name__ == "__main__": + app.run(host="0.0.0.0", port=42425, debug=True) diff --git a/script/static/logo.png b/script/static/logo.png new file mode 100644 index 0000000..6ba0066 Binary files /dev/null and b/script/static/logo.png differ diff --git a/script/static/wifi-icon.png b/script/static/wifi-icon.png new file mode 100644 index 0000000..42455bc Binary files /dev/null and b/script/static/wifi-icon.png differ diff --git a/script/templates/index.html b/script/templates/index.html new file mode 100644 index 0000000..6054578 --- /dev/null +++ b/script/templates/index.html @@ -0,0 +1,83 @@ + + + + + + Voucher Generator + + + +
+ +
Bitte wähle die gewünschte Dauer für den WLAN-Zugang 😀
+ +
+ + \ No newline at end of file diff --git a/script/templates/voucher.html b/script/templates/voucher.html new file mode 100644 index 0000000..daf2b9e --- /dev/null +++ b/script/templates/voucher.html @@ -0,0 +1,99 @@ + + + + + + + + Voucher + + + + +
+ Der {{ minutes }}min Zugangscode: +
+
+ {{ code }} +
+ + + + \ No newline at end of file