certbot richtet die automatische Verlaengerung (certbot.timer, zweimal taeglich) zwar schon beim Paket-Install selbst ein -- das war fuer den Admin in der Weboberflaeche bisher aber unsichtbar, reine Behauptung im Code-Kommentar. Jetzt sichtbar/nachpruefbar: - Neue Zeile "Automatische Verlaengerung" im Zertifikat-Status (nur bei Let's-Encrypt-Zertifikaten): Timer aktiv/inaktiv + naechster geplanter Lauf (_certbot_timer_status(), liest systemctl is-active/list-timers). - "Verlaengerung testen (Dry-Run)" -- certbot renew --dry-run, aendert nichts, zeigt nur ob die Verlaengerung im Ernstfall funktionieren wuerde. - "Jetzt manuell verlaengern" -- certbot renew --force-renewal, fuer Testzwecke oder eine sofortige Schluessel-Rotation (mit Warnhinweis auf Let's Encrypts Rate-Limits). - Hinweistext beim Anfordern zeigt den Timer-Status schon VOR der ersten Anforderung. VERSION 1.0.7 -> 1.0.8 (ab hier wieder normale fortlaufende Versionierung statt erneutem Release derselben Versionsnummer). Live auf Testsystem-Update-Restart verifiziert: Seite laedt weiterhin fehlerfrei (_certbot_timer_status crasht nicht ohne aktives Let's-Encrypt-Zertifikat), certbot.timer selbst bestaetigt aktiv (naechster Lauf laut systemctl list-timers korrekt angezeigt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
207 lines
12 KiB
HTML
207 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 %}
|
|
<div class="settings-grid" style="grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));">
|
|
|
|
<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)</label>
|
|
<input type="text" name="server_name" id="server_name" value="{{ server_name }}" placeholder="_ (kein bestimmter Hostname) oder z.B. tesm.example.com">
|
|
<div class="field-hint">"_" 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.</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<div class="field" style="flex:1;">
|
|
<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" style="flex:1;">
|
|
<label for="https_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-hint" style="margin-top:-8px; margin-bottom:14px;">
|
|
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.
|
|
</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)</span>
|
|
</label>
|
|
<div class="field-hint">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.</div>
|
|
</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)</label>
|
|
<input type="file" name="key_file" id="key_file" accept=".pem,.key" required>
|
|
<div class="field-hint">Ein passwortgeschütztes Schlüssel wird abgelehnt — nginx könnte ihn beim Start ohnehin nicht ohne manuelle Passworteingabe laden.</div>
|
|
</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 %}
|