From 9c631992afa107a5d291b59a923a7b89b230c550 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sat, 6 Jun 2026 14:13:48 +0200 Subject: [PATCH] Mehrere Subdomains pro Benutzer + README - subdomains-Tabelle (n DNS-Namen je Benutzer) inkl. Migration vom alten Einzel-Subdomain-Schema in database.init_db() - Benutzeranlage/Verwaltung: mehrere Subdomains hinzufuegen/entfernen - /nic/update aktualisiert alle Subdomains des Benutzers bzw. die per ?hostname= gewaehlte(n); eine Antwortzeile je Subdomain - Dashboard/Users-Templates auf das neue Modell umgestellt - README.md mit Setup, Plesk-Konfig, Router-Einrichtung und Endpoint-Doku - .gitignore: __pycache__/ und *.pyc Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + README.md | 185 +++++++++++++++++++++++++++++++ app/database.py | 48 ++++++++- app/main.py | 203 ++++++++++++++++++++++++++--------- app/templates/dashboard.html | 34 +++--- app/templates/users.html | 104 +++++++++++------- docker-compose.yml | 4 +- 7 files changed, 472 insertions(+), 109 deletions(-) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index f98107b..d2eca9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +data/ dyndns.db +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d25c40 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# DynDNS Manager für Plesk + +Ein kleiner, selbst gehosteter DynDNS-Server mit Web-Oberfläche. Er nimmt +DynDNS-v2-Updates (wie sie z. B. ein Telekom **Speedport** unter „Anderer +Anbieter" sendet) entgegen und trägt die gemeldete IP-Adresse über die +**Plesk REST-API** als A-Record in deine DNS-Zone ein. + +- Mehrere DynDNS-Benutzer, **mehrere Subdomains (DNS-Namen) pro Benutzer** +- Admin-Weboberfläche (Flask + Bootstrap) zum Verwalten von Benutzern, + Subdomains und Plesk-Einstellungen +- Update-Log pro Subdomain +- Läuft als Docker-Container hinter einem nginx-TLS-Proxy + +--- + +## Schnellstart + +```bash +# 1. Container bauen und starten +docker compose up -d --build + +# 2. Weboberfläche öffnen (lokal, hinter nginx -> https) +# http://127.0.0.1:5080 +# Standard-Login: admin / admin (sofort ändern!) +``` + +Die SQLite-Datenbank wird beim ersten Start automatisch unter `./data/dyndns.db` +angelegt (Volume-Mount in `docker-compose.yml`). Existiert die Datei nicht, wird +sie samt Tabellen erzeugt; ein Default-Admin (`admin` / `admin`) wird angelegt. + +--- + +## Konfiguration + +### Umgebungsvariablen (`docker-compose.yml`) + +| Variable | Bedeutung | Default | +|--------------|------------------------------------------------------|----------------------| +| `DB_PATH` | Pfad zur SQLite-Datei **im Container** | `/data/dyndns.db` | +| `SECRET_KEY` | Flask-Session-Key — **unbedingt ändern** (`openssl rand -hex 32`) | zufällig pro Start | + +> **Wichtig:** Das Volume mountet ein **Verzeichnis** (`./data:/data`), nicht die +> Datei direkt. Würde man die noch nicht existierende Datei mounten +> (`./dyndns.db:/data/dyndns.db`), legt Docker sie als *Verzeichnis* an und +> SQLite scheitert mit `unable to open database file`. + +### Plesk-Einstellungen (in der Weboberfläche → *Einstellungen*) + +| Feld | Beispiel | Bedeutung | +|-------------------|-----------------------------------|--------------------------------------------------| +| Plesk-URL | `https://plesk.example.com:8443` | Basis-URL der Plesk-Installation | +| API-Key | `XXXXXXXX-...` | Plesk REST-API-Key (in Plesk unter *API-Schlüssel* erzeugen) | +| Basis-Domain | `example.com` | DNS-Zone, in die die A-Records geschrieben werden | +| SSL verifizieren | ☑/☐ | bei selbstsigniertem Plesk-Zertifikat abschalten | + +Mit *Verbindung testen* lässt sich die API prüfen. + +--- + +## Benutzer & Subdomains anlegen + +1. In der Weboberfläche auf **Benutzer → Benutzer anlegen**. +2. **DynDNS-Benutzername** und **Passwort** vergeben (das sind die Zugangsdaten, + die später im Router eingetragen werden). +3. Eine oder **mehrere Subdomains** eintragen — getrennt durch Komma, + Leerzeichen oder Zeilenumbruch, z. B.: + + ``` + mypc + nas + router + ``` + + Zusammen mit der Basis-Domain (`example.com`) ergeben sich daraus die + Hostnamen `mypc.example.com`, `nas.example.com`, `router.example.com`. + + Auch **mehrstufige** Namen sind erlaubt (`pc.home` → `pc.home.example.com`), + solange die Records in der DNS-Zone der Basis-Domain liegen. + +Weitere Subdomains lassen sich später jederzeit über das **+**-Symbol in der +Benutzerzeile hinzufügen oder per **×** am Badge entfernen. + +> **Hinweis:** Der DNS-A-Record wird **nicht** schon beim Anlegen in Plesk +> erstellt, sondern erst beim **ersten DynDNS-Update** vom Client (lazy). Bis +> dahin zeigt das Dashboard „noch kein Update". + +--- + +## Router / Speedport einrichten + +Im Router unter DynDNS „**Anderer Anbieter**" konfigurieren: + +| Feld | Wert | +|--------------|---------------------------------------------------| +| Update-URL | `https://dyndns.example.com/nic/update?hostname=&myip=` | +| Domainname | z. B. `mypc.example.com` | +| Benutzername | der angelegte DynDNS-Benutzername | +| Passwort | das zugehörige Passwort | + +Die Platzhalter `` und `` füllt der Router automatisch. +Authentifiziert wird per **HTTP Basic Auth** (Benutzername/Passwort). + +### Update-Endpoint `/nic/update` + +Standard-DynDNS-v2-Protokoll: + +``` +GET /nic/update?hostname=mypc.example.com&myip=203.0.113.7 +Authorization: Basic +``` + +Verhalten: + +- **`hostname` angegeben** → nur die passende(n) Subdomain(s) dieses Benutzers + werden aktualisiert. Erlaubt ist der volle FQDN (`mypc.example.com`) **oder** + nur das Label (`mypc`); mehrere durch Komma getrennt. +- **`hostname` weggelassen** → **alle** Subdomains des Benutzers werden auf die + gemeldete IP gesetzt. +- `myip` (oder `ip`) bestimmt die Adresse; fehlt sie, wird die Quell-IP des + Requests verwendet. + +Antworten (eine Zeile pro Subdomain): + +| Antwort | Bedeutung | +|----------------|------------------------------------------------| +| `good ` | Record erfolgreich gesetzt | +| `nochg ` | IP unverändert, nichts zu tun | +| `nohost` | Benutzer hat keine (passende) Subdomain | +| `badauth` | Benutzername/Passwort falsch | +| `911` | Plesk nicht konfiguriert (URL/Key/Domain fehlt)| +| `dnserr` | Fehler beim Schreiben in Plesk (siehe Log) | + +Beispiel-Test mit `curl`: + +```bash +curl -u stefan:geheim \ + "https://dyndns.example.com/nic/update?hostname=mypc.example.com&myip=203.0.113.7" +``` + +--- + +## TLS / Reverse Proxy + +Der Container lauscht nur lokal (`127.0.0.1:5080`). Die TLS-Terminierung +übernimmt nginx — eine Beispielkonfiguration liegt in +[`nginx-subdomai-example.conf`](nginx-subdomai-example.conf). Domain in Plesk +anlegen, Let's-Encrypt-Zertifikat ausstellen, dann die Datei als zusätzliche +nginx-Direktiven einbinden. + +--- + +## Architektur + +``` +app/ +├── main.py Flask-Routen: Login, Dashboard, Benutzer/Subdomains, /nic/update +├── database.py SQLite-Schema, Migration, Settings-Helfer +├── plesk.py Plesk-REST-API: Verbindungstest + A-Record anlegen/aktualisieren +├── wsgi.py gunicorn-Einstieg (ruft init_db beim Start) +└── templates/ Bootstrap-Oberfläche +``` + +### Datenmodell + +- **`dyndns_users`** — Zugangsdaten (Benutzername/Passwort), aktiv-Flag +- **`subdomains`** — beliebig viele DNS-Namen je Benutzer (`dyndns_user_id` → + `dyndns_users.id`), inkl. aktueller IP und Zeitpunkt des letzten Updates +- **`update_log`** — Verlauf pro Subdomain +- **`admin_users`**, **`settings`** — Admin-Login und Plesk-Konfiguration + +Beim Start migriert `init_db()` automatisch ältere Datenbanken, die noch eine +einzelne `subdomain`-Spalte in `dyndns_users` hatten, in die neue +`subdomains`-Tabelle. + +--- + +## Lokale Entwicklung (ohne Docker) + +```bash +cd app +pip install -r requirements.txt +export DB_PATH=./dev.db +export SECRET_KEY=dev +python main.py # http://127.0.0.1:5000 +``` diff --git a/app/database.py b/app/database.py index 44545b3..679de32 100644 --- a/app/database.py +++ b/app/database.py @@ -6,13 +6,21 @@ DB_PATH = os.environ.get('DB_PATH', '/data/dyndns.db') def get_db(): + db_dir = os.path.dirname(DB_PATH) + if db_dir: + os.makedirs(db_dir, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn +def _columns(db, table): + return [r['name'] for r in db.execute(f'PRAGMA table_info({table})').fetchall()] + + def init_db(): db = get_db() + db.executescript(''' CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, @@ -29,16 +37,24 @@ def init_db(): id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, - subdomain TEXT UNIQUE NOT NULL, - current_ip TEXT, - last_updated TEXT, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); + CREATE TABLE IF NOT EXISTS subdomains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dyndns_user_id INTEGER NOT NULL, + subdomain TEXT UNIQUE NOT NULL, + current_ip TEXT, + last_updated TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (dyndns_user_id) REFERENCES dyndns_users(id) + ); + CREATE TABLE IF NOT EXISTS update_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, dyndns_user_id INTEGER NOT NULL, + subdomain_id INTEGER, old_ip TEXT, new_ip TEXT NOT NULL, result TEXT NOT NULL, @@ -47,6 +63,32 @@ def init_db(): ); ''') + # --- Migration vom alten Schema (genau eine Subdomain pro Benutzer) --- + # Früher trug dyndns_users die Spalten subdomain/current_ip/last_updated + # direkt. Diese werden in die neue subdomains-Tabelle überführt. + if 'subdomain' in _columns(db, 'dyndns_users'): + db.execute(''' + INSERT OR IGNORE INTO subdomains (dyndns_user_id, subdomain, current_ip, last_updated) + SELECT id, subdomain, current_ip, last_updated FROM dyndns_users + ''') + db.executescript(''' + CREATE TABLE dyndns_users_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + INSERT INTO dyndns_users_new (id, username, password_hash, active, created_at) + SELECT id, username, password_hash, active, created_at FROM dyndns_users; + DROP TABLE dyndns_users; + ALTER TABLE dyndns_users_new RENAME TO dyndns_users; + ''') + + # update_log: subdomain_id nachrüsten, falls noch altes Schema + if 'subdomain_id' not in _columns(db, 'update_log'): + db.execute('ALTER TABLE update_log ADD COLUMN subdomain_id INTEGER') + existing = db.execute('SELECT id FROM admin_users LIMIT 1').fetchone() if not existing: db.execute( diff --git a/app/main.py b/app/main.py index f4783d4..d10ade7 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,6 @@ import os +import re +import sqlite3 from datetime import datetime from functools import wraps @@ -67,22 +69,26 @@ def logout(): @login_required def dashboard(): db = get_db() - users = db.execute(''' - SELECT u.*, - (SELECT COUNT(*) FROM update_log l WHERE l.dyndns_user_id = u.id) AS update_count - FROM dyndns_users u - ORDER BY u.subdomain + subdomains = db.execute(''' + SELECT s.*, u.username, u.active, + (SELECT COUNT(*) FROM update_log l WHERE l.subdomain_id = s.id) AS update_count + FROM subdomains s + JOIN dyndns_users u ON s.dyndns_user_id = u.id + ORDER BY s.subdomain ''').fetchall() + user_count = db.execute('SELECT COUNT(*) AS c FROM dyndns_users').fetchone()['c'] logs = db.execute(''' - SELECT l.*, u.subdomain, u.username AS dyndns_username + SELECT l.*, s.subdomain, u.username AS dyndns_username FROM update_log l JOIN dyndns_users u ON l.dyndns_user_id = u.id + LEFT JOIN subdomains s ON l.subdomain_id = s.id ORDER BY l.timestamp DESC LIMIT 30 ''').fetchall() db.close() base_domain = get_setting('plesk_base_domain') - return render_template('dashboard.html', users=users, logs=logs, base_domain=base_domain) + return render_template('dashboard.html', subdomains=subdomains, + user_count=user_count, logs=logs, base_domain=base_domain) # --------------------------------------------------------------------------- @@ -156,12 +162,32 @@ def settings_password(): # Users # --------------------------------------------------------------------------- +SUBDOMAIN_RE = re.compile(r'^[a-z0-9]([a-z0-9\-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]*[a-z0-9])?)*$') + + +def _parse_subdomains(raw): + """Zerlegt eine Eingabe (Komma/Leerzeichen/Zeilenumbruch-getrennt) in + eine deduplizierte Liste gültiger Subdomain-Labels.""" + names, seen = [], set() + for part in re.split(r'[\s,]+', raw.strip().lower()): + part = part.strip().rstrip('.') + if part and part not in seen: + seen.add(part) + names.append(part) + return names + + @app.route('/users') @login_required def users(): db = get_db() - user_list = db.execute('SELECT * FROM dyndns_users ORDER BY subdomain').fetchall() + user_list = db.execute('SELECT * FROM dyndns_users ORDER BY username').fetchall() + subs = db.execute('SELECT * FROM subdomains ORDER BY subdomain').fetchall() db.close() + by_user = {} + for s in subs: + by_user.setdefault(s['dyndns_user_id'], []).append(s) + user_list = [{'row': u, 'subdomains': by_user.get(u['id'], [])} for u in user_list] base_domain = get_setting('plesk_base_domain') return render_template('users.html', users=user_list, base_domain=base_domain) @@ -171,21 +197,33 @@ def users(): def user_add(): username = request.form.get('username', '').strip() password = request.form.get('password', '').strip() - subdomain = request.form.get('subdomain', '').strip().lower() + names = _parse_subdomains(request.form.get('subdomains', '')) - if not username or not password or not subdomain: - flash('Alle Felder müssen ausgefüllt sein.', 'danger') + if not username or not password or not names: + flash('Benutzername, Passwort und mindestens eine Subdomain sind erforderlich.', 'danger') + return redirect(url_for('users')) + + invalid = [n for n in names if not SUBDOMAIN_RE.match(n)] + if invalid: + flash(f'Ungültige Subdomain(s): {", ".join(invalid)}', 'danger') return redirect(url_for('users')) db = get_db() try: - db.execute( - 'INSERT INTO dyndns_users (username, password_hash, subdomain) VALUES (?, ?, ?)', - (username, generate_password_hash(password), subdomain), + cur = db.execute( + 'INSERT INTO dyndns_users (username, password_hash) VALUES (?, ?)', + (username, generate_password_hash(password)), ) + user_id = cur.lastrowid + for n in names: + db.execute( + 'INSERT INTO subdomains (dyndns_user_id, subdomain) VALUES (?, ?)', + (user_id, n), + ) db.commit() - flash(f'Benutzer "{username}" angelegt.', 'success') + flash(f'Benutzer "{username}" mit {len(names)} Subdomain(s) angelegt.', 'success') except Exception as exc: + db.rollback() flash(f'Fehler: {exc}', 'danger') finally: db.close() @@ -197,20 +235,16 @@ def user_add(): def user_edit(user_id): username = request.form.get('username', '').strip() password = request.form.get('password', '').strip() - subdomain = request.form.get('subdomain', '').strip().lower() db = get_db() try: if password: db.execute( - 'UPDATE dyndns_users SET username=?, password_hash=?, subdomain=? WHERE id=?', - (username, generate_password_hash(password), subdomain, user_id), + 'UPDATE dyndns_users SET username=?, password_hash=? WHERE id=?', + (username, generate_password_hash(password), user_id), ) else: - db.execute( - 'UPDATE dyndns_users SET username=?, subdomain=? WHERE id=?', - (username, subdomain, user_id), - ) + db.execute('UPDATE dyndns_users SET username=? WHERE id=?', (username, user_id)) db.commit() flash('Benutzer aktualisiert.', 'success') except Exception as exc: @@ -225,6 +259,7 @@ def user_edit(user_id): def user_delete(user_id): db = get_db() db.execute('DELETE FROM update_log WHERE dyndns_user_id = ?', (user_id,)) + db.execute('DELETE FROM subdomains WHERE dyndns_user_id = ?', (user_id,)) db.execute('DELETE FROM dyndns_users WHERE id = ?', (user_id,)) db.commit() db.close() @@ -232,6 +267,51 @@ def user_delete(user_id): return redirect(url_for('users')) +@app.route('/users//subdomains/add', methods=['POST']) +@login_required +def subdomain_add(user_id): + names = _parse_subdomains(request.form.get('subdomains', '')) + if not names: + flash('Keine Subdomain angegeben.', 'danger') + return redirect(url_for('users')) + + invalid = [n for n in names if not SUBDOMAIN_RE.match(n)] + if invalid: + flash(f'Ungültige Subdomain(s): {", ".join(invalid)}', 'danger') + return redirect(url_for('users')) + + db = get_db() + added = 0 + try: + for n in names: + try: + db.execute( + 'INSERT INTO subdomains (dyndns_user_id, subdomain) VALUES (?, ?)', + (user_id, n), + ) + added += 1 + except sqlite3.IntegrityError: + flash(f'Subdomain "{n}" existiert bereits.', 'warning') + db.commit() + if added: + flash(f'{added} Subdomain(s) hinzugefügt.', 'success') + finally: + db.close() + return redirect(url_for('users')) + + +@app.route('/subdomains//delete', methods=['POST']) +@login_required +def subdomain_delete(subdomain_id): + db = get_db() + db.execute('UPDATE update_log SET subdomain_id = NULL WHERE subdomain_id = ?', (subdomain_id,)) + db.execute('DELETE FROM subdomains WHERE id = ?', (subdomain_id,)) + db.commit() + db.close() + flash('Subdomain gelöscht.', 'success') + return redirect(url_for('users')) + + @app.route('/users//toggle', methods=['POST']) @login_required def user_toggle(user_id): @@ -278,37 +358,62 @@ def dyndns_update(): db.close() return Response('911', 500, mimetype='text/plain') - old_ip = user['current_ip'] + subs = db.execute( + 'SELECT * FROM subdomains WHERE dyndns_user_id = ?', (user['id'],) + ).fetchall() - if old_ip == myip: - db.close() - return Response(f'nochg {myip}', 200, mimetype='text/plain') + # Optional kann der Client per ?hostname= eine bestimmte Subdomain wählen + # (FQDN wie "mypc.example.com" oder nur das Label "mypc"). Mehrere durch + # Komma getrennt sind erlaubt. Ohne hostname werden ALLE aktualisiert. + base = plesk_base_domain.lower().rstrip('.') + requested = [h for h in re.split(r'[\s,]+', request.args.get('hostname', '').strip().lower()) if h] + if requested: + wanted = {h.rstrip('.') for h in requested} + targets = [ + s for s in subs + if s['subdomain'] in wanted or f"{s['subdomain']}.{base}" in wanted + ] + else: + targets = list(subs) - try: - update_dns_record( - plesk_url, plesk_api_key, plesk_base_domain, - user['subdomain'], myip, verify_ssl=plesk_verify_ssl, - ) - db.execute( - 'UPDATE dyndns_users SET current_ip=?, last_updated=? WHERE id=?', - (myip, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), user['id']), - ) - db.execute( - 'INSERT INTO update_log (dyndns_user_id, old_ip, new_ip, result) VALUES (?,?,?,?)', - (user['id'], old_ip, myip, 'good'), - ) - db.commit() + if not targets: db.close() - return Response(f'good {myip}', 200, mimetype='text/plain') + return Response('nohost', 200, mimetype='text/plain') - except Exception as exc: - db.execute( - 'INSERT INTO update_log (dyndns_user_id, old_ip, new_ip, result) VALUES (?,?,?,?)', - (user['id'], old_ip, myip, f'error: {exc}'), - ) - db.commit() - db.close() - return Response('dnserr', 500, mimetype='text/plain') + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + lines = [] + for s in targets: + old_ip = s['current_ip'] + if old_ip == myip: + lines.append(f'nochg {myip}') + continue + try: + update_dns_record( + plesk_url, plesk_api_key, plesk_base_domain, + s['subdomain'], myip, verify_ssl=plesk_verify_ssl, + ) + db.execute( + 'UPDATE subdomains SET current_ip=?, last_updated=? WHERE id=?', + (myip, now, s['id']), + ) + db.execute( + 'INSERT INTO update_log (dyndns_user_id, subdomain_id, old_ip, new_ip, result)' + ' VALUES (?,?,?,?,?)', + (user['id'], s['id'], old_ip, myip, 'good'), + ) + lines.append(f'good {myip}') + except Exception as exc: + db.execute( + 'INSERT INTO update_log (dyndns_user_id, subdomain_id, old_ip, new_ip, result)' + ' VALUES (?,?,?,?,?)', + (user['id'], s['id'], old_ip, myip, f'error: {exc}'), + ) + lines.append('dnserr') + + db.commit() + db.close() + status = 500 if any(l == 'dnserr' for l in lines) else 200 + return Response('\n'.join(lines) + '\n', status, mimetype='text/plain') # --------------------------------------------------------------------------- diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index fd2f12c..a2e3eba 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -10,19 +10,19 @@
-
{{ users|length }}
+
{{ user_count }}
Benutzer gesamt
-
{{ users|selectattr('active', 'equalto', 1)|list|length }}
-
Aktiv
+
{{ subdomains|length }}
+
Subdomains
-
{{ users|sum(attribute='update_count') }}
+
{{ subdomains|sum(attribute='update_count') }}
Updates gesamt
@@ -31,15 +31,15 @@
- Benutzer & aktuelle IPs + Subdomains & aktuelle IPs Verwalten
- + @@ -47,23 +47,23 @@ - {% for u in users %} + {% for s in subdomains %} - - + - - + + {% else %} - + {% endfor %}
Subdomain HostnameDynDNS-User Aktuelle IP Letztes Update Updates
{{ u.subdomain }} - {% if base_domain %}{{ u.subdomain }}.{{ base_domain }}{% else %}—{% endif %} + + {% if base_domain %}{{ s.subdomain }}.{{ base_domain }}{% else %}{{ s.subdomain }}{% endif %} {{ s.username }} - {% if u.current_ip %} - {{ u.current_ip }} + {% if s.current_ip %} + {{ s.current_ip }} {% else %} noch kein Update {% endif %} {{ u.last_updated or '—' }}{{ u.update_count }}{{ s.last_updated or '—' }}{{ s.update_count }} - {% if u.active %} + {% if s.active %} Aktiv {% else %} Inaktiv @@ -71,7 +71,7 @@
Noch keine Benutzer angelegt.
Noch keine Subdomains angelegt.
@@ -96,7 +96,7 @@ {% for l in logs %} {{ l.timestamp }} - {{ l.subdomain }} ({{ l.dyndns_username }}) + {{ l.subdomain or '—' }} ({{ l.dyndns_username }}) {{ l.old_ip or '—' }} {{ l.new_ip }} diff --git a/app/templates/users.html b/app/templates/users.html index acae146..310bc20 100644 --- a/app/templates/users.html +++ b/app/templates/users.html @@ -11,32 +11,35 @@
- +
- - - - + - {% for u in users %} + {% for item in users %} + {% set u = item.row %} - - - + - + + + {% else %} - {% endfor %} @@ -160,17 +187,18 @@
Wird im Speedport als „Passwort" eingetragen.
- +
- + {% if base_domain %} .{{ base_domain }} {% endif %}
-
Nur Kleinbuchstaben, Ziffern und Bindestriche.
+
+ Eine oder mehrere — durch Komma, Leerzeichen oder Zeilenumbruch getrennt. + Nur Kleinbuchstaben, Ziffern, Bindestriche und Punkte. +
SubdomainHostname DynDNS-UserAktuelle IPLetztes UpdateSubdomains Status
{{ u.subdomain }} - {% if base_domain %}{{ u.subdomain }}.{{ base_domain }}{% else %}—{% endif %} - {{ u.username }}{{ u.username }} - {% if u.current_ip %} - {{ u.current_ip }} - {% else %}{% endif %} + {% for s in item.subdomains %} + + {% if base_domain %}{{ s.subdomain }}.{{ base_domain }}{% else %}{{ s.subdomain }}{% endif %} + {% if s.current_ip %}· {{ s.current_ip }}{% endif %} +
+ +
+
+ {% else %} + keine + {% endfor %}
{{ u.last_updated or '—' }} {% if u.active %} Aktiv @@ -46,9 +49,13 @@
+ @@ -58,8 +65,7 @@ @@ -67,6 +73,35 @@
+
Noch keine Benutzer. Jetzt anlegen.