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) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,4 @@
|
|||||||
|
data/
|
||||||
dyndns.db
|
dyndns.db
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
@@ -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=<domain>&myip=<ipaddr>` |
|
||||||
|
| Domainname | z. B. `mypc.example.com` |
|
||||||
|
| Benutzername | der angelegte DynDNS-Benutzername |
|
||||||
|
| Passwort | das zugehörige Passwort |
|
||||||
|
|
||||||
|
Die Platzhalter `<domain>` und `<ipaddr>` 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 <user:pass>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <ip>` | Record erfolgreich gesetzt |
|
||||||
|
| `nochg <ip>` | 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
|
||||||
|
```
|
||||||
+45
-3
@@ -6,13 +6,21 @@ DB_PATH = os.environ.get('DB_PATH', '/data/dyndns.db')
|
|||||||
|
|
||||||
|
|
||||||
def get_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 = sqlite3.connect(DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def _columns(db, table):
|
||||||
|
return [r['name'] for r in db.execute(f'PRAGMA table_info({table})').fetchall()]
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
||||||
db.executescript('''
|
db.executescript('''
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@@ -29,16 +37,24 @@ def init_db():
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
subdomain TEXT UNIQUE NOT NULL,
|
|
||||||
current_ip TEXT,
|
|
||||||
last_updated TEXT,
|
|
||||||
active INTEGER NOT NULL DEFAULT 1,
|
active INTEGER NOT NULL DEFAULT 1,
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
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 (
|
CREATE TABLE IF NOT EXISTS update_log (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
dyndns_user_id INTEGER NOT NULL,
|
dyndns_user_id INTEGER NOT NULL,
|
||||||
|
subdomain_id INTEGER,
|
||||||
old_ip TEXT,
|
old_ip TEXT,
|
||||||
new_ip TEXT NOT NULL,
|
new_ip TEXT NOT NULL,
|
||||||
result 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()
|
existing = db.execute('SELECT id FROM admin_users LIMIT 1').fetchone()
|
||||||
if not existing:
|
if not existing:
|
||||||
db.execute(
|
db.execute(
|
||||||
|
|||||||
+154
-49
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
@@ -67,22 +69,26 @@ def logout():
|
|||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
users = db.execute('''
|
subdomains = db.execute('''
|
||||||
SELECT u.*,
|
SELECT s.*, u.username, u.active,
|
||||||
(SELECT COUNT(*) FROM update_log l WHERE l.dyndns_user_id = u.id) AS update_count
|
(SELECT COUNT(*) FROM update_log l WHERE l.subdomain_id = s.id) AS update_count
|
||||||
FROM dyndns_users u
|
FROM subdomains s
|
||||||
ORDER BY u.subdomain
|
JOIN dyndns_users u ON s.dyndns_user_id = u.id
|
||||||
|
ORDER BY s.subdomain
|
||||||
''').fetchall()
|
''').fetchall()
|
||||||
|
user_count = db.execute('SELECT COUNT(*) AS c FROM dyndns_users').fetchone()['c']
|
||||||
logs = db.execute('''
|
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
|
FROM update_log l
|
||||||
JOIN dyndns_users u ON l.dyndns_user_id = u.id
|
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
|
ORDER BY l.timestamp DESC
|
||||||
LIMIT 30
|
LIMIT 30
|
||||||
''').fetchall()
|
''').fetchall()
|
||||||
db.close()
|
db.close()
|
||||||
base_domain = get_setting('plesk_base_domain')
|
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
|
# 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')
|
@app.route('/users')
|
||||||
@login_required
|
@login_required
|
||||||
def users():
|
def users():
|
||||||
db = get_db()
|
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()
|
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')
|
base_domain = get_setting('plesk_base_domain')
|
||||||
return render_template('users.html', users=user_list, base_domain=base_domain)
|
return render_template('users.html', users=user_list, base_domain=base_domain)
|
||||||
|
|
||||||
@@ -171,21 +197,33 @@ def users():
|
|||||||
def user_add():
|
def user_add():
|
||||||
username = request.form.get('username', '').strip()
|
username = request.form.get('username', '').strip()
|
||||||
password = request.form.get('password', '').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:
|
if not username or not password or not names:
|
||||||
flash('Alle Felder müssen ausgefüllt sein.', 'danger')
|
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'))
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
try:
|
try:
|
||||||
db.execute(
|
cur = db.execute(
|
||||||
'INSERT INTO dyndns_users (username, password_hash, subdomain) VALUES (?, ?, ?)',
|
'INSERT INTO dyndns_users (username, password_hash) VALUES (?, ?)',
|
||||||
(username, generate_password_hash(password), subdomain),
|
(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()
|
db.commit()
|
||||||
flash(f'Benutzer "{username}" angelegt.', 'success')
|
flash(f'Benutzer "{username}" mit {len(names)} Subdomain(s) angelegt.', 'success')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
db.rollback()
|
||||||
flash(f'Fehler: {exc}', 'danger')
|
flash(f'Fehler: {exc}', 'danger')
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
@@ -197,20 +235,16 @@ def user_add():
|
|||||||
def user_edit(user_id):
|
def user_edit(user_id):
|
||||||
username = request.form.get('username', '').strip()
|
username = request.form.get('username', '').strip()
|
||||||
password = request.form.get('password', '').strip()
|
password = request.form.get('password', '').strip()
|
||||||
subdomain = request.form.get('subdomain', '').strip().lower()
|
|
||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
try:
|
try:
|
||||||
if password:
|
if password:
|
||||||
db.execute(
|
db.execute(
|
||||||
'UPDATE dyndns_users SET username=?, password_hash=?, subdomain=? WHERE id=?',
|
'UPDATE dyndns_users SET username=?, password_hash=? WHERE id=?',
|
||||||
(username, generate_password_hash(password), subdomain, user_id),
|
(username, generate_password_hash(password), user_id),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
db.execute(
|
db.execute('UPDATE dyndns_users SET username=? WHERE id=?', (username, user_id))
|
||||||
'UPDATE dyndns_users SET username=?, subdomain=? WHERE id=?',
|
|
||||||
(username, subdomain, user_id),
|
|
||||||
)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
flash('Benutzer aktualisiert.', 'success')
|
flash('Benutzer aktualisiert.', 'success')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -225,6 +259,7 @@ def user_edit(user_id):
|
|||||||
def user_delete(user_id):
|
def user_delete(user_id):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
db.execute('DELETE FROM update_log WHERE dyndns_user_id = ?', (user_id,))
|
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.execute('DELETE FROM dyndns_users WHERE id = ?', (user_id,))
|
||||||
db.commit()
|
db.commit()
|
||||||
db.close()
|
db.close()
|
||||||
@@ -232,6 +267,51 @@ def user_delete(user_id):
|
|||||||
return redirect(url_for('users'))
|
return redirect(url_for('users'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/users/<int:user_id>/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/<int:subdomain_id>/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/<int:user_id>/toggle', methods=['POST'])
|
@app.route('/users/<int:user_id>/toggle', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def user_toggle(user_id):
|
def user_toggle(user_id):
|
||||||
@@ -278,37 +358,62 @@ def dyndns_update():
|
|||||||
db.close()
|
db.close()
|
||||||
return Response('911', 500, mimetype='text/plain')
|
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:
|
# Optional kann der Client per ?hostname= eine bestimmte Subdomain wählen
|
||||||
db.close()
|
# (FQDN wie "mypc.example.com" oder nur das Label "mypc"). Mehrere durch
|
||||||
return Response(f'nochg {myip}', 200, mimetype='text/plain')
|
# 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:
|
if not targets:
|
||||||
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()
|
|
||||||
db.close()
|
db.close()
|
||||||
return Response(f'good {myip}', 200, mimetype='text/plain')
|
return Response('nohost', 200, mimetype='text/plain')
|
||||||
|
|
||||||
except Exception as exc:
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
db.execute(
|
lines = []
|
||||||
'INSERT INTO update_log (dyndns_user_id, old_ip, new_ip, result) VALUES (?,?,?,?)',
|
for s in targets:
|
||||||
(user['id'], old_ip, myip, f'error: {exc}'),
|
old_ip = s['current_ip']
|
||||||
)
|
if old_ip == myip:
|
||||||
db.commit()
|
lines.append(f'nochg {myip}')
|
||||||
db.close()
|
continue
|
||||||
return Response('dnserr', 500, mimetype='text/plain')
|
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')
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="card text-center p-3">
|
<div class="card text-center p-3">
|
||||||
<div class="fs-2 fw-bold text-primary">{{ users|length }}</div>
|
<div class="fs-2 fw-bold text-primary">{{ user_count }}</div>
|
||||||
<div class="text-muted small">Benutzer gesamt</div>
|
<div class="text-muted small">Benutzer gesamt</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="card text-center p-3">
|
<div class="card text-center p-3">
|
||||||
<div class="fs-2 fw-bold text-success">{{ users|selectattr('active', 'equalto', 1)|list|length }}</div>
|
<div class="fs-2 fw-bold text-success">{{ subdomains|length }}</div>
|
||||||
<div class="text-muted small">Aktiv</div>
|
<div class="text-muted small">Subdomains</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="card text-center p-3">
|
<div class="card text-center p-3">
|
||||||
<div class="fs-2 fw-bold text-info">{{ users|sum(attribute='update_count') }}</div>
|
<div class="fs-2 fw-bold text-info">{{ subdomains|sum(attribute='update_count') }}</div>
|
||||||
<div class="text-muted small">Updates gesamt</div>
|
<div class="text-muted small">Updates gesamt</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,15 +31,15 @@
|
|||||||
<!-- User table -->
|
<!-- User table -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span class="fw-semibold">Benutzer & aktuelle IPs</span>
|
<span class="fw-semibold">Subdomains & aktuelle IPs</span>
|
||||||
<a href="{{ url_for('users') }}" class="btn btn-sm btn-outline-primary">Verwalten</a>
|
<a href="{{ url_for('users') }}" class="btn btn-sm btn-outline-primary">Verwalten</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Subdomain</th>
|
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
|
<th>DynDNS-User</th>
|
||||||
<th>Aktuelle IP</th>
|
<th>Aktuelle IP</th>
|
||||||
<th>Letztes Update</th>
|
<th>Letztes Update</th>
|
||||||
<th>Updates</th>
|
<th>Updates</th>
|
||||||
@@ -47,23 +47,23 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for u in users %}
|
{% for s in subdomains %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-semibold">{{ u.subdomain }}</td>
|
<td class="fw-semibold font-monospace small">
|
||||||
<td class="text-muted small">
|
{% if base_domain %}{{ s.subdomain }}.{{ base_domain }}{% else %}{{ s.subdomain }}{% endif %}
|
||||||
{% if base_domain %}{{ u.subdomain }}.{{ base_domain }}{% else %}—{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ s.username }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if u.current_ip %}
|
{% if s.current_ip %}
|
||||||
<span class="badge bg-secondary badge-ip">{{ u.current_ip }}</span>
|
<span class="badge bg-secondary badge-ip">{{ s.current_ip }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">noch kein Update</span>
|
<span class="text-muted">noch kein Update</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted small">{{ u.last_updated or '—' }}</td>
|
<td class="text-muted small">{{ s.last_updated or '—' }}</td>
|
||||||
<td>{{ u.update_count }}</td>
|
<td>{{ s.update_count }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if u.active %}
|
{% if s.active %}
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Inaktiv</span>
|
<span class="badge bg-secondary">Inaktiv</span>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="6" class="text-center text-muted py-4">Noch keine Benutzer angelegt.</td></tr>
|
<tr><td colspan="6" class="text-center text-muted py-4">Noch keine Subdomains angelegt.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
{% for l in logs %}
|
{% for l in logs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted small">{{ l.timestamp }}</td>
|
<td class="text-muted small">{{ l.timestamp }}</td>
|
||||||
<td>{{ l.subdomain }} <span class="text-muted small">({{ l.dyndns_username }})</span></td>
|
<td>{{ l.subdomain or '—' }} <span class="text-muted small">({{ l.dyndns_username }})</span></td>
|
||||||
<td class="badge-ip text-muted small">{{ l.old_ip or '—' }}</td>
|
<td class="badge-ip text-muted small">{{ l.old_ip or '—' }}</td>
|
||||||
<td><span class="badge bg-secondary badge-ip">{{ l.new_ip }}</span></td>
|
<td><span class="badge bg-secondary badge-ip">{{ l.new_ip }}</span></td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
+66
-38
@@ -11,32 +11,35 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0 align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Subdomain</th>
|
|
||||||
<th>Hostname</th>
|
|
||||||
<th>DynDNS-User</th>
|
<th>DynDNS-User</th>
|
||||||
<th>Aktuelle IP</th>
|
<th>Subdomains</th>
|
||||||
<th>Letztes Update</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for u in users %}
|
{% for item in users %}
|
||||||
|
{% set u = item.row %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-semibold">{{ u.subdomain }}</td>
|
<td class="fw-semibold">{{ u.username }}</td>
|
||||||
<td class="text-muted small font-monospace">
|
|
||||||
{% if base_domain %}{{ u.subdomain }}.{{ base_domain }}{% else %}—{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ u.username }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if u.current_ip %}
|
{% for s in item.subdomains %}
|
||||||
<span class="badge bg-secondary font-monospace">{{ u.current_ip }}</span>
|
<span class="badge bg-light text-dark border me-1 mb-1 font-monospace">
|
||||||
{% else %}<span class="text-muted">—</span>{% endif %}
|
{% if base_domain %}{{ s.subdomain }}.{{ base_domain }}{% else %}{{ s.subdomain }}{% endif %}
|
||||||
|
{% if s.current_ip %}<span class="text-muted">· {{ s.current_ip }}</span>{% endif %}
|
||||||
|
<form method="post" action="{{ url_for('subdomain_delete', subdomain_id=s.id) }}"
|
||||||
|
class="d-inline" onsubmit="return confirm('Subdomain {{ s.subdomain }} löschen?');">
|
||||||
|
<button type="submit" class="btn btn-link btn-sm p-0 ms-1 text-danger" title="Subdomain löschen"
|
||||||
|
style="vertical-align: baseline;"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</form>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">keine</span>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted small">{{ u.last_updated or '—' }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if u.active %}
|
{% if u.active %}
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
@@ -46,9 +49,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-success"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#subModal{{ u.id }}"
|
||||||
|
title="Subdomain hinzufügen">
|
||||||
|
<i class="bi bi-plus-lg"></i>
|
||||||
|
</button>
|
||||||
<button class="btn btn-outline-secondary"
|
<button class="btn btn-outline-secondary"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal" data-bs-target="#editModal{{ u.id }}"
|
||||||
data-bs-target="#editModal{{ u.id }}"
|
|
||||||
title="Bearbeiten">
|
title="Bearbeiten">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -58,8 +65,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<button class="btn btn-outline-danger"
|
<button class="btn btn-outline-danger"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal" data-bs-target="#delModal{{ u.id }}"
|
||||||
data-bs-target="#delModal{{ u.id }}"
|
|
||||||
title="Löschen">
|
title="Löschen">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -67,6 +73,35 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<!-- Add-subdomain modal -->
|
||||||
|
<div class="modal fade" id="subModal{{ u.id }}" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Subdomain(s) zu „{{ u.username }}" hinzufügen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('subdomain_add', user_id=u.id) }}">
|
||||||
|
<div class="modal-body">
|
||||||
|
<label class="form-label">Subdomain(s)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea name="subdomains" class="form-control font-monospace" rows="3"
|
||||||
|
placeholder="mypc nas router" required></textarea>
|
||||||
|
{% if base_domain %}
|
||||||
|
<span class="input-group-text text-muted">.{{ base_domain }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Mehrere durch Komma, Leerzeichen oder Zeilenumbruch trennen.</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Edit modal -->
|
<!-- Edit modal -->
|
||||||
<div class="modal fade" id="editModal{{ u.id }}" tabindex="-1">
|
<div class="modal fade" id="editModal{{ u.id }}" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@@ -86,16 +121,8 @@
|
|||||||
<label class="form-label">Neues Passwort <span class="text-muted">(leer = unverändert)</span></label>
|
<label class="form-label">Neues Passwort <span class="text-muted">(leer = unverändert)</span></label>
|
||||||
<input name="password" type="password" class="form-control">
|
<input name="password" type="password" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-text">Subdomains werden direkt in der Tabelle per
|
||||||
<label class="form-label">Subdomain</label>
|
<i class="bi bi-plus-lg"></i> / <i class="bi bi-x-lg"></i> verwaltet.</div>
|
||||||
<div class="input-group">
|
|
||||||
<input name="subdomain" type="text" class="form-control font-monospace"
|
|
||||||
value="{{ u.subdomain }}" required>
|
|
||||||
{% if base_domain %}
|
|
||||||
<span class="input-group-text text-muted">.{{ base_domain }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
@@ -115,8 +142,8 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<strong>{{ u.username }}</strong> ({{ u.subdomain }}) wirklich löschen?
|
<strong>{{ u.username }}</strong> mit {{ item.subdomains|length }} Subdomain(s)
|
||||||
Alle Update-Logs werden ebenfalls entfernt.
|
wirklich löschen? Alle Update-Logs werden ebenfalls entfernt.
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
@@ -129,7 +156,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="7" class="text-center text-muted py-4">
|
<tr><td colspan="4" class="text-center text-muted py-4">
|
||||||
Noch keine Benutzer. <a href="#" data-bs-toggle="modal" data-bs-target="#addModal">Jetzt anlegen.</a>
|
Noch keine Benutzer. <a href="#" data-bs-toggle="modal" data-bs-target="#addModal">Jetzt anlegen.</a>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -160,17 +187,18 @@
|
|||||||
<div class="form-text">Wird im Speedport als „Passwort" eingetragen.</div>
|
<div class="form-text">Wird im Speedport als „Passwort" eingetragen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Subdomain</label>
|
<label class="form-label">Subdomain(s)</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input name="subdomain" type="text" class="form-control font-monospace"
|
<textarea name="subdomains" class="form-control font-monospace" rows="3"
|
||||||
placeholder="mypc" required
|
placeholder="mypc nas router" required></textarea>
|
||||||
pattern="[a-z0-9]([a-z0-9\-]*[a-z0-9])?"
|
|
||||||
title="Kleinbuchstaben, Ziffern und Bindestriche">
|
|
||||||
{% if base_domain %}
|
{% if base_domain %}
|
||||||
<span class="input-group-text text-muted">.{{ base_domain }}</span>
|
<span class="input-group-text text-muted">.{{ base_domain }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text">Nur Kleinbuchstaben, Ziffern und Bindestriche.</div>
|
<div class="form-text">
|
||||||
|
Eine oder mehrere — durch Komma, Leerzeichen oder Zeilenumbruch getrennt.
|
||||||
|
Nur Kleinbuchstaben, Ziffern, Bindestriche und Punkte.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
+2
-2
@@ -4,9 +4,9 @@ services:
|
|||||||
container_name: dyndns-manager
|
container_name: dyndns-manager
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:5080:5000" # nur lokal – nginx macht den TLS-Termination
|
- "5080:5000" # nur lokal – nginx macht den TLS-Termination
|
||||||
environment:
|
environment:
|
||||||
- DB_PATH=/data/dyndns.db
|
- DB_PATH=/data/dyndns.db
|
||||||
- SECRET_KEY=BITTE_AENDERN_32_ZEICHEN_RANDOM # openssl rand -hex 32
|
- SECRET_KEY=BITTE_AENDERN_32_ZEICHEN_RANDOM # openssl rand -hex 32
|
||||||
volumes:
|
volumes:
|
||||||
- ./dyndns.db:/data/dyndns.db # SQLite liegt neben der docker-compose.yml
|
- ./data:/data # SQLite-DB landet in ./data/dyndns.db (Verzeichnis-Mount!)
|
||||||
|
|||||||
Reference in New Issue
Block a user