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:
Stefan Hacker
2026-06-06 14:13:48 +02:00
parent 2542cf5455
commit 9c631992af
7 changed files with 472 additions and 109 deletions
+45 -3
View File
@@ -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(
+154 -49
View File
@@ -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/<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'])
@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')
# ---------------------------------------------------------------------------
+17 -17
View File
@@ -10,19 +10,19 @@
<div class="row g-3 mb-4">
<div class="col-sm-4">
<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>
</div>
<div class="col-sm-4">
<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="text-muted small">Aktiv</div>
<div class="fs-2 fw-bold text-success">{{ subdomains|length }}</div>
<div class="text-muted small">Subdomains</div>
</div>
</div>
<div class="col-sm-4">
<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>
</div>
@@ -31,15 +31,15 @@
<!-- User table -->
<div class="card mb-4">
<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>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Subdomain</th>
<th>Hostname</th>
<th>DynDNS-User</th>
<th>Aktuelle IP</th>
<th>Letztes Update</th>
<th>Updates</th>
@@ -47,23 +47,23 @@
</tr>
</thead>
<tbody>
{% for u in users %}
{% for s in subdomains %}
<tr>
<td class="fw-semibold">{{ u.subdomain }}</td>
<td class="text-muted small">
{% if base_domain %}{{ u.subdomain }}.{{ base_domain }}{% else %}—{% endif %}
<td class="fw-semibold font-monospace small">
{% if base_domain %}{{ s.subdomain }}.{{ base_domain }}{% else %}{{ s.subdomain }}{% endif %}
</td>
<td>{{ s.username }}</td>
<td>
{% if u.current_ip %}
<span class="badge bg-secondary badge-ip">{{ u.current_ip }}</span>
{% if s.current_ip %}
<span class="badge bg-secondary badge-ip">{{ s.current_ip }}</span>
{% else %}
<span class="text-muted">noch kein Update</span>
{% endif %}
</td>
<td class="text-muted small">{{ u.last_updated or '—' }}</td>
<td>{{ u.update_count }}</td>
<td class="text-muted small">{{ s.last_updated or '—' }}</td>
<td>{{ s.update_count }}</td>
<td>
{% if u.active %}
{% if s.active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Inaktiv</span>
@@ -71,7 +71,7 @@
</td>
</tr>
{% 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 %}
</tbody>
</table>
@@ -96,7 +96,7 @@
{% for l in logs %}
<tr>
<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><span class="badge bg-secondary badge-ip">{{ l.new_ip }}</span></td>
<td>
+66 -38
View File
@@ -11,32 +11,35 @@
<div class="card">
<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">
<tr>
<th>Subdomain</th>
<th>Hostname</th>
<th>DynDNS-User</th>
<th>Aktuelle IP</th>
<th>Letztes Update</th>
<th>Subdomains</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for u in users %}
{% for item in users %}
{% set u = item.row %}
<tr>
<td class="fw-semibold">{{ u.subdomain }}</td>
<td class="text-muted small font-monospace">
{% if base_domain %}{{ u.subdomain }}.{{ base_domain }}{% else %}—{% endif %}
</td>
<td>{{ u.username }}</td>
<td class="fw-semibold">{{ u.username }}</td>
<td>
{% if u.current_ip %}
<span class="badge bg-secondary font-monospace">{{ u.current_ip }}</span>
{% else %}<span class="text-muted"></span>{% endif %}
{% for s in item.subdomains %}
<span class="badge bg-light text-dark border me-1 mb-1 font-monospace">
{% 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 class="text-muted small">{{ u.last_updated or '—' }}</td>
<td>
{% if u.active %}
<span class="badge bg-success">Aktiv</span>
@@ -46,9 +49,13 @@
</td>
<td class="text-end">
<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"
data-bs-toggle="modal"
data-bs-target="#editModal{{ u.id }}"
data-bs-toggle="modal" data-bs-target="#editModal{{ u.id }}"
title="Bearbeiten">
<i class="bi bi-pencil"></i>
</button>
@@ -58,8 +65,7 @@
</button>
</form>
<button class="btn btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#delModal{{ u.id }}"
data-bs-toggle="modal" data-bs-target="#delModal{{ u.id }}"
title="Löschen">
<i class="bi bi-trash"></i>
</button>
@@ -67,6 +73,35 @@
</td>
</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&#10;nas&#10;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 -->
<div class="modal fade" id="editModal{{ u.id }}" tabindex="-1">
<div class="modal-dialog">
@@ -86,16 +121,8 @@
<label class="form-label">Neues Passwort <span class="text-muted">(leer = unverändert)</span></label>
<input name="password" type="password" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Subdomain</label>
<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 class="form-text">Subdomains werden direkt in der Tabelle per
<i class="bi bi-plus-lg"></i> / <i class="bi bi-x-lg"></i> verwaltet.</div>
</div>
<div class="modal-footer">
<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>
</div>
<div class="modal-body">
<strong>{{ u.username }}</strong> ({{ u.subdomain }}) wirklich löschen?
Alle Update-Logs werden ebenfalls entfernt.
<strong>{{ u.username }}</strong> mit {{ item.subdomains|length }} Subdomain(s)
wirklich löschen? Alle Update-Logs werden ebenfalls entfernt.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
@@ -129,7 +156,7 @@
</div>
{% 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>
</td></tr>
{% endfor %}
@@ -160,17 +187,18 @@
<div class="form-text">Wird im Speedport als „Passwort" eingetragen.</div>
</div>
<div class="mb-3">
<label class="form-label">Subdomain</label>
<label class="form-label">Subdomain(s)</label>
<div class="input-group">
<input name="subdomain" type="text" class="form-control font-monospace"
placeholder="mypc" required
pattern="[a-z0-9]([a-z0-9\-]*[a-z0-9])?"
title="Kleinbuchstaben, Ziffern und Bindestriche">
<textarea name="subdomains" class="form-control font-monospace" rows="3"
placeholder="mypc&#10;nas&#10;router" required></textarea>
{% if base_domain %}
<span class="input-group-text text-muted">.{{ base_domain }}</span>
{% endif %}
</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 class="modal-footer">