first commit
This commit is contained in:
commit
a1334ed865
12 changed files with 434 additions and 0 deletions
10
.env-dist
Normal file
10
.env-dist
Normal file
|
@ -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
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.env
|
||||
script/__pycache__/
|
||||
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
@ -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"]
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
|
@ -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
|
39
entrypoint.sh
Normal file
39
entrypoint.sh
Normal file
|
@ -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
|
BIN
lmn-logo.png
Normal file
BIN
lmn-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
flask
|
||||
gunicorn
|
||||
authlib
|
||||
requests
|
||||
pyunifi
|
||||
pillow
|
||||
qrcode
|
157
script/app.py
Normal file
157
script/app.py
Normal file
|
@ -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/<int:minutes>")
|
||||
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)
|
BIN
script/static/logo.png
Normal file
BIN
script/static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
BIN
script/static/wifi-icon.png
Normal file
BIN
script/static/wifi-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
83
script/templates/index.html
Normal file
83
script/templates/index.html
Normal file
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Voucher Generator</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #85b5f8;
|
||||
}
|
||||
.container {
|
||||
margin-top: 0px;
|
||||
}
|
||||
.logo {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.text {
|
||||
font-size: 24px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.button {
|
||||
padding: 20px 40px;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
background-color: #0051a7;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 14px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.zeit {
|
||||
font-size: 3em;
|
||||
margin-top: 10px;
|
||||
text-shadow: 0 6px 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.text {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.buttons {
|
||||
flex-direction: column; /* Buttons untereinander anordnen */
|
||||
gap: 10px; /* Abstand zwischen den Buttons */
|
||||
margin-left: 20px; /* Zentrieren der Buttons */
|
||||
margin-right: 20px;
|
||||
}
|
||||
.button {
|
||||
width: auto; /* Buttons so breit wie der Bildschirm (mit etwas Abstand) */
|
||||
margin-left: 20px ;
|
||||
margin-right: 20px ;
|
||||
font-size: 1rem;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="{{ logo }}" alt="Logo" class="logo">
|
||||
<div class="text">Bitte wähle die gewünschte Dauer für den WLAN-Zugang 😀</div>
|
||||
<div class="buttons">
|
||||
<a href="/voucher/45" class="button">WLAN-Zugangscode<br>für<br><span class="zeit">45min</span><br>erstellen</a>
|
||||
<a href="/voucher/90" class="button">WLAN-Zugangscode<br>für<br><span class="zeit">90min</span><br>erstellen</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
99
script/templates/voucher.html
Normal file
99
script/templates/voucher.html
Normal file
|
@ -0,0 +1,99 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<title>Voucher</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #ffe14b;
|
||||
/* Gedeckt gelber Hintergrund */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.code {
|
||||
text-align: center;
|
||||
font-size: 9rem;
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
margin: 20px 0;
|
||||
word-spacing: 10px;
|
||||
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wifi-symbol {
|
||||
position: relative;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.wifi-icon {
|
||||
width: 30%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 25%;
|
||||
max-width: 1200px;
|
||||
margin: 50px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px; /* Abstand zwischen den Grafiken */
|
||||
}
|
||||
|
||||
.wifi-icon,
|
||||
.qr-code {
|
||||
width: 80%; /* Grafiken nehmen 80% der Bildschirmbreite ein */
|
||||
max-width: 300px; /* Maximale Breite der Grafiken */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
Der {{ minutes }}min Zugangscode:
|
||||
</div>
|
||||
<div class="code">
|
||||
{{ code }}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<img src="{{ wifi_icon }}" alt="Logo" class="wifi-icon">
|
||||
<img src="data:image/png;base64,{{ qr_code|b64encode }}" alt="QR-Code" class="qr-code">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Add table
Reference in a new issue