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:
+154
-49
@@ -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')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user