Statisch sichtbare Erklaerungstexte (.hint/.field-hint) app-weit auf ein
einheitliches "i"-Icon umgestellt: Hover zeigt den Text neben dem
Mauszeiger, Klick/Tap oeffnet ein wiederverwendetes Modal mit dem vollen
Text. Zentral in app.js (initHintIcons, ensureHintTooltip/-Modal) via
Event-Delegation, kein Aufwand pro Seite. Hover ist bewusst per
matchMedia("(hover: hover) and (pointer: fine)") gated -- auf Touch-
Geraeten faellt automatisch nur Tap-zu-Modal an, da echte
mouseenter/mousemove-Events dort ohnehin nicht zuverlaessig auftreten.
Nicht jede .hint/.field-hint wurde umgestellt -- nach folgenden Kriterien
entschieden:
- Live-Status-Anzeigen (aktuelle Log-Pfade, Netzwerkstatus, Auditlog-
Schwellenwerte, aktive Reservierungs-/Lease-Zaehler, certbot-Timer-
Status usw.) bleiben sichtbar, da sie aktuellen Zustand zeigen, keine
generische Erklaerung.
- Kurze, einzeilige Abschnitts-Untertitel (z.B. "Server, Bind-Konto und
Suchparameter fuer die Anbindung an AD/LDAP.") bleiben als Orientierung
direkt unter der Ueberschrift sichtbar.
- Bereits JS-live-aktualisierte Status-Felder (id-tragend, z.B.
"Fragt live... ab." -> "N Gruppen geladen.") bleiben unangetastet.
- Laengere, mehrsaetzige Abschnitts-Erklaerungen (z.B. AD-Gruppenzu-
ordnungen, Fileshare-Gruppenzuordnung, Subnetze, DHCP-Options, Wartung)
sowie so gut wie alle Feld-Erklaerungen wandern ins Icon.
Nebenbei: Karte "Fileshare-Gruppen" in LDAP-Einstellungen zu "Fileshare-
Gruppenzuordnung" umbenannt (passt jetzt zur bereits bestehenden
log_action-Zielbezeichnung in app.py).
Neue gemeinsame Macro-Datei templates/_hint_icon.html, importiert von
15 Templates. Verifiziert: alle 18 betroffenen Seiten laden fehlerfrei
(200, keine JS-Fehler), Icon-Anzahl je Seite geprueft, Hover-Tooltip und
Klick-Modal auf Desktop, Tap-Modal auf simuliertem Mobilgeraet (390x844,
Touch), sowie stichprobenartig bestaetigt dass die bewusst belassenen
Hinweise unveraendert sichtbar bleiben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
201 lines
12 KiB
HTML
201 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "settings_nginx" %}
|
|
{% set can_edit = current_user.has_permission('settings_nginx.edit') %}
|
|
{% block page_title %}NGINX{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">Reverse-Proxy: Domain, Ports, SSL/HSTS und Zertifikat</div>{% endblock %}
|
|
|
|
{% block content %}
|
|
{% import "_hint_icon.html" as hi %}
|
|
<div class="settings-grid settings-grid--wide">
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Reverse-Proxy</h2>
|
|
<div class="hint">Domain (server_name), Ports und HTTPS/HSTS für den nginx-Reverse-Proxy vor der App.</div>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary btn-sm"
|
|
onclick="openRawLogModal('{{ url_for('settings_nginx_raw') }}', 'Aktuelle nginx-Konfiguration (RAW)')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
|
|
RAW-Konfiguration
|
|
</button>
|
|
</div>
|
|
|
|
{% if pending_nginx_token %}
|
|
<div class="card-pad" style="background:var(--warning-dim); border-radius:var(--radius-sm); margin-bottom:16px;">
|
|
<p style="margin:0 0 12px; font-size:13px;">
|
|
Neue nginx-Konfiguration wurde angewendet. Wenn diese Seite gerade noch lädt, funktioniert die Verbindung —
|
|
bitte bestätigen, bevor automatisch zurückgerollt wird (nach {{ nginx_revert_seconds }}s ohne Bestätigung).
|
|
</p>
|
|
<form method="post">
|
|
<input type="hidden" name="confirm_nginx" value="{{ pending_nginx_token }}">
|
|
<button type="submit" class="btn btn-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Verbindung funktioniert — bestätigen
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% elif can_edit %}
|
|
<form method="post"
|
|
data-confirm="nginx-Konfiguration wirklich ändern? Falls die Verbindung danach abbricht, wird die vorherige Konfiguration automatisch nach {{ nginx_revert_seconds }} Sekunden wiederhergestellt.">
|
|
<div class="field">
|
|
<label for="server_name">Domain (server_name) {{ hi.hint_icon("\"_\" ist nginx' Catch-all (Standard für interne Instanzen ohne eigene Domain) — für Let's Encrypt muss hier die tatsächliche, öffentlich auflösbare Domain stehen.", "Domain (server_name)") }}</label>
|
|
<input type="text" name="server_name" id="server_name" value="{{ server_name }}" placeholder="_ (kein bestimmter Hostname) oder z.B. tesm.example.com">
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<div class="field field--half">
|
|
<label for="http_port">HTTP-Port</label>
|
|
<input type="number" name="http_port" id="http_port" min="1" max="65535" value="{{ http_port }}" required>
|
|
</div>
|
|
<div class="field field--half">
|
|
<label for="https_port">HTTPS-Port {{ hi.hint_icon("Let's Encrypt validiert IMMER über Port 80 (protokollbedingt, unabhängig vom hier eingestellten HTTP-Port) — weicht der HTTP-Port von 80 ab, wird dafür automatisch zusätzlich ein minimaler Port-80-Block mitgeschrieben.", "HTTP-Port / HTTPS-Port") }}</label>
|
|
<input type="number" name="https_port" id="https_port" min="1" max="65535" value="{{ https_port }}" required>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="switch-check" style="display:inline-flex; align-items:center; gap:8px;">
|
|
<input type="checkbox" name="ssl_enabled" id="ssl_enabled_check" {% if ssl_enabled %}checked{% endif %} {% if not cert_info %}disabled{% endif %}>
|
|
<span class="track"></span>
|
|
<span>SSL/HTTPS aktivieren</span>
|
|
</label>
|
|
{% if not cert_info %}<div class="field-hint">Erst nach Hochladen oder Anfordern eines Zertifikats verfügbar.</div>{% endif %}
|
|
</div>
|
|
<div class="field">
|
|
<label class="switch-check" style="display:inline-flex; align-items:center; gap:8px;">
|
|
<input type="checkbox" name="hsts_enabled" id="hsts_enabled_check" {% if hsts_enabled %}checked{% endif %} {% if not ssl_enabled %}disabled{% endif %}>
|
|
<span class="track"></span>
|
|
<span>HSTS (Strict-Transport-Security) {{ hi.hint_icon("Weist Browser an, diese Instanz künftig NUR noch über HTTPS aufzurufen — bleibt auch bei einem späteren Zurückschalten auf HTTP im Browser für 1 Jahr bestehen. Nur aktivieren, wenn das Zertifikat dauerhaft gepflegt wird.", "HSTS") }}</span>
|
|
</label>
|
|
</div>
|
|
<button type="submit" name="apply_nginx" value="1" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 12l2 2 4-4"/><path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/></svg>
|
|
Anwenden
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<p class="text-faint" style="font-size:12.5px;">Für Änderungen fehlt das Recht „NGINX ändern“.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Zertifikat</h2>
|
|
<div class="hint">Hochladen (PEM) oder automatisch per Let's Encrypt anfordern — mit automatischer Verlängerung über certbots eigenen Timer.</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if cert_info %}
|
|
<div class="detail-list" style="margin-bottom:18px;">
|
|
<div class="detail-row"><span class="k">Quelle</span><span class="v">{{ "Let's Encrypt" if cert_info.source == "letsencrypt" else "Hochgeladen" }}</span></div>
|
|
<div class="detail-row"><span class="k">Subject</span><span class="v" style="text-align:right; word-break:break-all;">{{ cert_info.subject }}</span></div>
|
|
<div class="detail-row"><span class="k">Aussteller</span><span class="v" style="text-align:right; word-break:break-all;">{{ cert_info.issuer }}</span></div>
|
|
<div class="detail-row">
|
|
<span class="k">Gültig bis</span>
|
|
<span class="v">
|
|
{{ cert_info.not_after.strftime('%d.%m.%Y') }}
|
|
{% if cert_info.days_left < 0 %}
|
|
<span class="pill offline" style="margin-left:6px;">abgelaufen</span>
|
|
{% elif cert_info.days_left <= 30 %}
|
|
<span class="pill unknown" style="margin-left:6px;">noch {{ cert_info.days_left }} Tage</span>
|
|
{% else %}
|
|
<span class="pill online" style="margin-left:6px;">noch {{ cert_info.days_left }} Tage</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
{% if cert_info.source == "letsencrypt" %}
|
|
<div class="detail-row">
|
|
<span class="k">Automatische Verlängerung</span>
|
|
<span class="v">
|
|
{% if certbot_timer and certbot_timer.active %}
|
|
<span class="pill online">aktiv</span>
|
|
{% if certbot_timer.next_run %}<span class="text-faint" style="font-size:11px; display:block; margin-top:2px;">nächster Lauf: {{ certbot_timer.next_run }}</span>{% endif %}
|
|
{% elif certbot_timer %}
|
|
<span class="pill offline">inaktiv</span>
|
|
{% else %}
|
|
<span class="pill unknown">unbekannt</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if cert_info.source == "letsencrypt" and can_edit %}
|
|
<div class="flex gap-2" style="margin-bottom:18px;">
|
|
<form method="post" style="flex:1;">
|
|
<button type="submit" name="test_renewal" value="1" class="btn btn-secondary btn-block btn-sm">Verlängerung testen (Dry-Run)</button>
|
|
</form>
|
|
<form method="post" style="flex:1;" data-confirm="Zertifikat jetzt sofort erneuern (zählt gegen Let's Encrypts Rate-Limits)?">
|
|
<button type="submit" name="force_renew" value="1" class="btn btn-secondary btn-block btn-sm">Jetzt manuell verlängern</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px;">Noch kein Zertifikat hinterlegt.</p>
|
|
{% endif %}
|
|
|
|
{% if can_edit %}
|
|
<div class="field">
|
|
<label style="font-weight:600; font-size:13px;">Let's Encrypt anfordern</label>
|
|
<div class="field-hint" style="margin-bottom:8px;">Setzt voraus, dass die Domain öffentlich auf diesen Host auflöst und Port 80 aus dem Internet erreichbar ist (HTTP-01-Validierung) — für rein interne Instanzen ohne öffentliche Domain nicht nutzbar, dann stattdessen hochladen. Verlängerung läuft danach automatisch über certbots eigenen Timer{% if certbot_timer %} ({{ "aktiv" if certbot_timer.active else "inaktiv!" }}){% endif %}, zweimal täglich.</div>
|
|
</div>
|
|
<form method="post" style="margin-bottom:18px;">
|
|
<div class="field">
|
|
<label for="le_domain">Domain</label>
|
|
<input type="text" name="le_domain" id="le_domain" placeholder="z.B. tesm.example.com" value="{{ server_name if server_name != '_' else '' }}">
|
|
</div>
|
|
<div class="field">
|
|
<label for="le_email">E-Mail (optional, für Ablauf-Erinnerungen)</label>
|
|
<input type="email" name="le_email" id="le_email" placeholder="admin@example.com">
|
|
</div>
|
|
<button type="submit" name="request_letsencrypt" value="1" class="btn btn-secondary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/><path d="M9 12l2 2 4-4"/></svg>
|
|
Zertifikat anfordern
|
|
</button>
|
|
</form>
|
|
|
|
<div class="field"><label style="font-weight:600; font-size:13px;">Manuell hochladen</label></div>
|
|
<form method="post" enctype="multipart/form-data" style="margin-bottom:10px;">
|
|
<div class="field">
|
|
<label for="cert_file">Zertifikat (PEM, .crt/.pem)</label>
|
|
<input type="file" name="cert_file" id="cert_file" accept=".pem,.crt,.cer" required>
|
|
</div>
|
|
<div class="field">
|
|
<label for="key_file">Privater Schlüssel (PEM, unverschlüsselt) {{ hi.hint_icon("Ein passwortgeschütztes Schlüssel wird abgelehnt — nginx könnte ihn beim Start ohnehin nicht ohne manuelle Passworteingabe laden.", "Privater Schlüssel") }}</label>
|
|
<input type="file" name="key_file" id="key_file" accept=".pem,.key" required>
|
|
</div>
|
|
<button type="submit" name="upload_cert" value="1" class="btn btn-secondary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/></svg>
|
|
Zertifikat + Schlüssel hochladen
|
|
</button>
|
|
</form>
|
|
|
|
{% if cert_info and not ssl_enabled %}
|
|
<form method="post" data-confirm="Hinterlegtes Zertifikat wirklich entfernen?" data-no-unsaved-guard>
|
|
<button type="submit" name="remove_cert" value="1" class="btn btn-danger btn-block">Zertifikat entfernen</button>
|
|
</form>
|
|
{% elif cert_info %}
|
|
<p class="text-faint" style="font-size:11.5px;">SSL muss zuerst deaktiviert werden, bevor das Zertifikat entfernt werden kann.</p>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="text-faint" style="font-size:12.5px;">Für Anfordern/Upload/Entfernen fehlt das Recht „NGINX ändern“.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const sslCheck = document.getElementById("ssl_enabled_check");
|
|
const hstsCheck = document.getElementById("hsts_enabled_check");
|
|
if (sslCheck && hstsCheck) {
|
|
sslCheck.addEventListener("change", () => {
|
|
hstsCheck.disabled = !sslCheck.checked;
|
|
if (!sslCheck.checked) hstsCheck.checked = false;
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|