Umlaute in der Oberflaeche, Sitzungskopplung, Statusanzeigen
Echte Umlaute in allen Texten der Oberflaeche und in den Markdown-Dateien. Die Einbindung von Dateifreigaben haengt jetzt an der Sitzung: wird sie widerrufen oder laeuft sie ab, verschwinden die Freigaben mit ihr. Neu einstellbar ist die automatische Abmeldung bei Leerlauf (Vorgabe 30 Minuten). Ein Geraet, das nicht antwortet, bietet nur noch "Einschalten (PoE)" an -- Neustart ueber SSH oder RPC braucht ein erreichbares Geraet. Der Zeitgeber der naechsten Pruefung und die Restlaufzeit der Lizenz stehen jetzt oben in der Kopfzeile; der doppelte Knopf darunter ist weg. Millisekunden werden ab einer Sekunde als Sekunden und ab einer Minute als m:ss dargestellt. Ausserdem: Release-Tarball nicht mehr im Repository -- er haengt am Release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
# TESM-Lizenzserver
|
# TESM-Lizenzserver
|
||||||
|
|
||||||
Ausgabe und Verwaltung der Lizenzen fuer TESM: Kunde -> Ticket -> Lizenz,
|
Ausgabe und Verwaltung der Lizenzen für TESM: Kunde -> Ticket -> Lizenz,
|
||||||
Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand ueber
|
Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand über
|
||||||
MS Graph und eine Hierarchie aus Unter-Lizenzservern.
|
MS Graph und eine Hierarchie aus Unter-Lizenzservern.
|
||||||
|
|
||||||
**Dieses Repository enthaelt genau das, was zum Installieren und Betreiben des
|
**Dieses Repository enthält genau das, was zum Installieren und Betreiben des
|
||||||
Lizenzservers gebraucht wird** -- nichts darueber hinaus:
|
Lizenzservers gebraucht wird** -- nichts darüber hinaus:
|
||||||
|
|
||||||
```
|
```
|
||||||
apps/tesm-license die Anwendung
|
apps/tesm-license die Anwendung
|
||||||
packages/tesm-core gemeinsamer Kern (Anmeldung, Rechte, Sicherung, nginx, ...)
|
packages/tesm-core gemeinsamer Kern (Anmeldung, Rechte, Sicherung, nginx, ...)
|
||||||
packages/tesm-licensing Lizenzprotokoll, gemeinsame Haelfte
|
packages/tesm-licensing Lizenzprotokoll, gemeinsame Hälfte
|
||||||
packages/tesm-licensing-server Lizenzprotokoll, Serverhaelfte: ausstellen, erneuern, antworten
|
packages/tesm-licensing-server Lizenzprotokoll, Serverhälfte: ausstellen, erneuern, antworten
|
||||||
deploy/ install.sh, update.sh, release.sh, systemd, privilegierter Helfer
|
deploy/ install.sh, update.sh, release.sh, systemd, privilegierter Helfer
|
||||||
docs/ Betrieb und Sicherheit
|
docs/ Betrieb und Sicherheit
|
||||||
```
|
```
|
||||||
@@ -28,7 +28,7 @@ Die verwaltete Anwendung liegt hier **nicht** mit drin.
|
|||||||
| Zweig | Inhalt |
|
| Zweig | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `main` | dieser Neubau (Version 2.0.0) |
|
| `main` | dieser Neubau (Version 2.0.0) |
|
||||||
| `SONNET5` | der Stand davor, unveraendert aufbewahrt |
|
| `SONNET5` | der Stand davor, unverändert aufbewahrt |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,18 +41,18 @@ sudo -u tesm-license tesm-license-admin bootstrap
|
|||||||
sudo -u tesm-license tesm-license-admin create-admin
|
sudo -u tesm-license tesm-license-admin create-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
**`init-key` erzeugt den Signaturschluessel dieses Servers. Sichern Sie ihn
|
**`init-key` erzeugt den Signaturschlüssel dieses Servers. Sichern Sie ihn
|
||||||
sofort.** Ohne ihn laesst sich keine ausgestellte Lizenz mehr erneuern, und
|
sofort.** Ohne ihn lässt sich keine ausgestellte Lizenz mehr erneuern, und
|
||||||
Kunden muessten einen Ausstellerwechsel bestaetigen.
|
Kunden müssten einen Ausstellerwechsel bestätigen.
|
||||||
|
|
||||||
Zielzustand: **Port 443 mit TLS, Port 80 leitet dorthin um** -- ausser dem
|
Zielzustand: **Port 443 mit TLS, Port 80 leitet dorthin um** -- ausser dem
|
||||||
ACME-Pfad, der erreichbar bleiben muss, sonst kann Let's Encrypt nie erneuern.
|
ACME-Pfad, der erreichbar bleiben muss, sonst kann Let's Encrypt nie erneuern.
|
||||||
Sitzt ein eigener Reverse Proxy davor, der TLS beendet, laeuft der Dienst ohne
|
Sitzt ein eigener Reverse Proxy davor, der TLS beendet, läuft der Dienst ohne
|
||||||
`--https` schlicht auf Port 80.
|
`--https` schlicht auf Port 80.
|
||||||
|
|
||||||
Der Lizenzserver darf neben TESM auf demselben Host laufen; nginx unterscheidet
|
Der Lizenzserver darf neben TESM auf demselben Host laufen; nginx unterscheidet
|
||||||
die beiden am Servernamen, und jede Installation hat ihren eigenen `SITE_KEY`
|
die beiden am Servernamen, und jede Installation hat ihren eigenen `SITE_KEY`
|
||||||
fuer nginx-Site, logrotate, Zertifikate und Sitzungscookie.
|
für nginx-Site, logrotate, Zertifikate und Sitzungscookie.
|
||||||
|
|
||||||
### Aktualisieren
|
### Aktualisieren
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ fuer nginx-Site, logrotate, Zertifikate und Sitzungscookie.
|
|||||||
sudo bash deploy/update.sh --app tesm-license
|
sudo bash deploy/update.sh --app tesm-license
|
||||||
```
|
```
|
||||||
|
|
||||||
Vor jedem Update entsteht ein vollstaendiges Backup, danach laeuft ein
|
Vor jedem Update entsteht ein vollständiges Backup, danach läuft ein
|
||||||
Health-Check mit automatischem Rollback.
|
Health-Check mit automatischem Rollback.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -69,16 +69,16 @@ Health-Check mit automatischem Rollback.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u tesm-license tesm-license-admin status
|
sudo -u tesm-license tesm-license-admin status
|
||||||
sudo -u tesm-license tesm-license-admin init-key # Signaturschluessel
|
sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel
|
||||||
sudo -u tesm-license tesm-license-admin bootstrap # Grunddaten
|
sudo -u tesm-license tesm-license-admin bootstrap # Grunddaten
|
||||||
sudo -u tesm-license tesm-license-admin create-admin
|
sudo -u tesm-license tesm-license-admin create-admin
|
||||||
sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand uebernehmen
|
sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand übernehmen
|
||||||
```
|
```
|
||||||
|
|
||||||
`migrate-legacy` liest die SQLite-Datei eines alten Lizenzservers **nur
|
`migrate-legacy` liest die SQLite-Datei eines alten Lizenzservers **nur
|
||||||
lesend**, uebernimmt dessen Signaturschluessel und stellt die Lizenzen mit ihrer
|
lesend**, übernimmt dessen Signaturschlüssel und stellt die Lizenzen mit ihrer
|
||||||
restlichen Laufzeit neu aus. Wird der Schluessel nicht uebernommen, muessen
|
restlichen Laufzeit neu aus. Wird der Schlüssel nicht übernommen, müssen
|
||||||
Kunden den Ausstellerwechsel einmal bestaetigen.
|
Kunden den Ausstellerwechsel einmal bestätigen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,14 +101,14 @@ genau einmal.
|
|||||||
(`tesm/license-file/v2\0`) -- eine Signatur aus einem Kontext ist in einem
|
(`tesm/license-file/v2\0`) -- eine Signatur aus einem Kontext ist in einem
|
||||||
anderen nicht verwendbar.
|
anderen nicht verwendbar.
|
||||||
* Kanonisches JSON, damit dieselbe Aussage genau eine Signatur hat.
|
* Kanonisches JSON, damit dieselbe Aussage genau eine Signatur hat.
|
||||||
* Nonce gegen Wiedereinspielung, `seq` gegen das Zurueckrollen auf eine aeltere
|
* Nonce gegen Wiedereinspielung, `seq` gegen das Zurückrollen auf eine ältere
|
||||||
Lizenz.
|
Lizenz.
|
||||||
* Aussteller wird beim ersten Kontakt gemerkt (TOFU); ein Wechsel muss
|
* Aussteller wird beim ersten Kontakt gemerkt (TOFU); ein Wechsel muss
|
||||||
ausdruecklich bestaetigt werden.
|
ausdrücklich bestätigt werden.
|
||||||
* Der Fingerabdruck der Maschine enthaelt **keinen** Hostnamen -- Umbenennen
|
* Der Fingerabdruck der Maschine enthält **keinen** Hostnamen -- Umbenennen
|
||||||
brach im Vorgaenger die Bindung.
|
brach im Vorgänger die Bindung.
|
||||||
* Offline-Aktivierung ueber einen kurzen Code in beide Richtungen; die Lizenz
|
* Offline-Aktivierung über einen kurzen Code in beide Richtungen; die Lizenz
|
||||||
bleibt auf dem Server als "Aktivierung offen" markiert, bis sie zurueckkommt.
|
bleibt auf dem Server als "Aktivierung offen" markiert, bis sie zurückkommt.
|
||||||
* Keine Lizenz ohne `master_endpoint`: eine Ausstellung ohne Endpunkt wird
|
* Keine Lizenz ohne `master_endpoint`: eine Ausstellung ohne Endpunkt wird
|
||||||
abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann.
|
abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann.
|
||||||
|
|
||||||
@@ -130,8 +130,8 @@ Entwicklungsrepository, ebenso die Tests.
|
|||||||
|
|
||||||
* **Keine doppelte `licensing.py`.** Ein Paket, das beide Anwendungen
|
* **Keine doppelte `licensing.py`.** Ein Paket, das beide Anwendungen
|
||||||
installieren -- statt zweier Dateien, die byte-identisch bleiben sollen.
|
installieren -- statt zweier Dateien, die byte-identisch bleiben sollen.
|
||||||
* **Kein root fuer die Anwendung.** Ein einziger allowlisted Helfer mit
|
* **Kein root für die Anwendung.** Ein einziger allowlisted Helfer mit
|
||||||
Argumentpruefung.
|
Argumentprüfung.
|
||||||
* **CSRF-Schutz ueberhaupt.** Der Vorgaenger hatte keinen.
|
* **CSRF-Schutz überhaupt.** Der Vorgänger hatte keinen.
|
||||||
* **Rechte werden serverseitig geprueft**, nicht nur im Template ausgeblendet.
|
* **Rechte werden serverseitig geprüft**, nicht nur im Template ausgeblendet.
|
||||||
* **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock.
|
* **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-license"
|
name = "tesm-license"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from .permissions import PERMISSIONS, ROLE_PRESETS
|
|||||||
from .schema import LICENSE_MIGRATIONS
|
from .schema import LICENSE_MIGRATIONS
|
||||||
from .settings_spec import LICENSE_SERVER_SETTINGS
|
from .settings_spec import LICENSE_SERVER_SETTINGS
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.0.2"
|
||||||
|
|
||||||
APP_KEY = "tesm-license"
|
APP_KEY = "tesm-license"
|
||||||
APP_NAME = "TESM-Lizenzserver"
|
APP_NAME = "TESM-Lizenzserver"
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ NAVIGATION = NavTree(
|
|||||||
),
|
),
|
||||||
NavItem(
|
NavItem(
|
||||||
key="logs_audit",
|
key="logs_audit",
|
||||||
label="Aenderungen",
|
label="Audit",
|
||||||
icon="history",
|
icon="history",
|
||||||
endpoint="logs.audit_view",
|
endpoint="logs.audit_view",
|
||||||
permissions=("logs_audit.view", "logs_audit.export"),
|
permissions=("logs_audit.view", "logs_audit.export"),
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% extends "tesm_core/base.html" %}
|
||||||
{% from "tesm_core/macros.html" import field, alert, pill %}
|
{% from "tesm_core/macros.html" import field, alert, pill %}
|
||||||
|
|
||||||
{% block page_title %}Sicherung und Uebertragung{% endblock %}
|
{% block page_title %}Sicherung und Übertragung{% endblock %}
|
||||||
{% block page_sub %}Verschluesselter Export · zweistufiger Import{% endblock %}
|
{% block page_sub %}Verschlüsselter Export · zweistufiger Import{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Der Export wird mit Argon2id aus Ihrer Passphrase verschluesselt (AES-256-GCM). Ohne
|
{{ alert('Der Export wird mit Argon2id aus Ihrer Passphrase verschlüsselt (AES-256-GCM). Ohne
|
||||||
diese Passphrase ist die Datei nicht wiederherstellbar -- es gibt keine Hintertuer.', 'warning',
|
diese Passphrase ist die Datei nicht wiederherstellbar -- es gibt keine Hintertür.', 'warning',
|
||||||
'Passphrase') }}
|
'Passphrase') }}
|
||||||
|
|
||||||
{{ alert('Der Signaturschluessel (' ~ signing_key_path ~ ') ist bewusst
|
{{ alert('Der Signaturschlüssel (' ~ signing_key_path ~ ') ist bewusst
|
||||||
nicht Teil dieser Sicherung. Er ist der Kern des Vertrauens und gehoert in eine
|
nicht Teil dieser Sicherung. Er ist der Kern des Vertrauens und gehört in eine
|
||||||
getrennte, bewusst verwahrte Sicherung -- nicht in dieselbe Datei wie die Kundendaten. Ohne ihn
|
getrennte, bewusst verwahrte Sicherung -- nicht in dieselbe Datei wie die Kundendaten. Ohne ihn
|
||||||
ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'danger',
|
ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr prüfbar.', 'danger',
|
||||||
'Wichtig: Schluessel getrennt sichern') }}
|
'Wichtig: Schlüssel getrennt sichern') }}
|
||||||
|
|
||||||
<div class="grid grid--split">
|
<div class="grid grid--split">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('download', size=17) }} Sicherung erstellen</div>
|
<div class="card__title">{{ icon('download', size=17) }} Sicherung erstellen</div>
|
||||||
<div class="card__sub">Verlangt eine frische Passwortbestaetigung</div>
|
<div class="card__sub">Verlangt eine frische Passwortbestätigung</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="{{ url_for('backup.export') }}" data-no-guard>
|
<form method="post" action="{{ url_for('backup.export') }}" data-no-guard>
|
||||||
@@ -40,7 +40,7 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
|
|||||||
{% if category.admin_only %}{{ pill('nur Admin', 'accent') }}{% endif %}
|
{% if category.admin_only %}{{ pill('nur Admin', 'accent') }}{% endif %}
|
||||||
<div class="fs-xs faint">{{ category.hint }}</div>
|
<div class="fs-xs faint">{{ category.hint }}</div>
|
||||||
{% if not allowed %}
|
{% if not allowed %}
|
||||||
<div class="fs-xs danger-text">Dafuer fehlt Ihnen das Recht.</div>
|
<div class="fs-xs danger-text">Dafür fehlt Ihnen das Recht.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -66,8 +66,8 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
|
|||||||
<section class="card card--accent">
|
<section class="card card--accent">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('upload', size=17) }} Geoeffnete Sicherung</div>
|
<div class="card__title">{{ icon('upload', size=17) }} Geöffnete Sicherung</div>
|
||||||
<div class="card__sub">Gueltig fuer {{ preview_ttl }} Minuten</div>
|
<div class="card__sub">Gültig für {{ preview_ttl }} Minuten</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="{{ url_for('backup.import_apply') }}">
|
<form method="post" action="{{ url_for('backup.import_apply') }}">
|
||||||
@@ -75,7 +75,7 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
|
|||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th></th><th>Kategorie</th><th class="numeric">Eintraege</th></tr></thead>
|
<thead><tr><th></th><th>Kategorie</th><th class="numeric">Einträge</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in preview.summary %}
|
{% for entry in preview.summary %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -89,17 +89,17 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{ alert('Bereits vorhandene Lizenzen werden nie ueberschrieben -- ihr Inhalt ist
|
{{ alert('Bereits vorhandene Lizenzen werden nie überschrieben -- ihr Inhalt ist
|
||||||
kryptographisch festgelegt. Kunden werden ueber ihren Namen zusammengefuehrt.',
|
kryptographisch festgelegt. Kunden werden über ihren Namen zusammengeführt.',
|
||||||
'info') }}
|
'info') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer modal__footer--split">
|
<div class="card__footer modal__footer--split">
|
||||||
<button class="btn btn--sm btn--ghost" type="submit"
|
<button class="btn btn--sm btn--ghost" type="submit"
|
||||||
formaction="{{ url_for('backup.import_discard') }}">Verwerfen</button>
|
formaction="{{ url_for('backup.import_discard') }}">Verwerfen</button>
|
||||||
<button class="btn btn--primary" type="submit"
|
<button class="btn btn--primary" type="submit"
|
||||||
data-confirm="Ausgewaehlte Kategorien jetzt uebernehmen?"
|
data-confirm="Ausgewählte Kategorien jetzt übernehmen?"
|
||||||
data-confirm-title="Sicherung uebernehmen" data-confirm-ok="Uebernehmen"
|
data-confirm-title="Sicherung übernehmen" data-confirm-ok="Übernehmen"
|
||||||
data-confirm-style="primary">Uebernehmen</button>
|
data-confirm-style="primary">Übernehmen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -108,7 +108,7 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('upload', size=17) }} Sicherung einspielen</div>
|
<div class="card__title">{{ icon('upload', size=17) }} Sicherung einspielen</div>
|
||||||
<div class="card__sub">Erst oeffnen und pruefen, dann uebernehmen</div>
|
<div class="card__sub">Erst öffnen und prüfen, dann übernehmen</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" enctype="multipart/form-data" data-no-guard
|
<form method="post" enctype="multipart/form-data" data-no-guard
|
||||||
@@ -120,8 +120,8 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
|
|||||||
autocomplete='off') }}
|
autocomplete='off') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
<button class="btn btn--primary" type="submit" data-busy-label="Oeffne ...">
|
<button class="btn btn--primary" type="submit" data-busy-label="Öffne ...">
|
||||||
Datei oeffnen
|
Datei öffnen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
{% if licenses %}
|
{% if licenses %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>Lizenz</th><th>Typ</th><th>Zustand</th><th>Laeuft ab</th><th>Host</th></tr></thead>
|
<thead><tr><th>Lizenz</th><th>Typ</th><th>Zustand</th><th>Läuft ab</th><th>Host</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in licenses %}
|
{% for entry in licenses %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{{ alert('Diese Daten verwaltet der Sub-Lizenzserver selbst. Sie sind hier rein
|
{{ alert('Diese Daten verwaltet der Sub-Lizenzserver selbst. Sie sind hier rein
|
||||||
informativ und werden bei jedem Bericht vollstaendig ersetzt.', 'info') }}
|
informativ und werden bei jedem Bericht vollständig ersetzt.', 'info') }}
|
||||||
{% for entry in reported.customers %}
|
{% for entry in reported.customers %}
|
||||||
<details class="card">
|
<details class="card">
|
||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header" style="cursor:pointer">
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<dl class="kv fs-xs">
|
<dl class="kv fs-xs">
|
||||||
<dt>Angelegt</dt><dd>{{ customer.created_at | dt }} von {{ customer.created_by or '-' }}</dd>
|
<dt>Angelegt</dt><dd>{{ customer.created_at | dt }} von {{ customer.created_by or '-' }}</dd>
|
||||||
<dt>Geaendert</dt><dd>{{ customer.updated_at | dt }}</dd>
|
<dt>Geändert</dt><dd>{{ customer.updated_at | dt }}</dd>
|
||||||
<dt>Portal-Zugang</dt>
|
<dt>Portal-Zugang</dt>
|
||||||
<dd><span class="mono break-all">{{ portal_url }}</span>
|
<dd><span class="mono break-all">{{ portal_url }}</span>
|
||||||
<button class="btn btn--sm btn--ghost" type="button" data-copy="{{ portal_url }}">
|
<button class="btn btn--sm btn--ghost" type="button" data-copy="{{ portal_url }}">
|
||||||
@@ -205,9 +205,9 @@
|
|||||||
{% if current_user.has('customers.delete') %}
|
{% if current_user.has('customers.delete') %}
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
formaction="{{ url_for('customers.delete', customer_id=customer.id) }}"
|
formaction="{{ url_for('customers.delete', customer_id=customer.id) }}"
|
||||||
data-confirm="Kunde {{ customer.name }} loeschen? Das ist nur moeglich, solange kein Ticket mehr an ihm haengt."
|
data-confirm="Kunde {{ customer.name }} löschen? Das ist nur möglich, solange kein Ticket mehr an ihm hängt."
|
||||||
data-confirm-title="Kunde loeschen" data-confirm-ok="Loeschen">
|
data-confirm-title="Kunde löschen" data-confirm-ok="Löschen">
|
||||||
Loeschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
{% else %}<span></span>{% endif %}
|
{% else %}<span></span>{% endif %}
|
||||||
<button class="btn btn--primary" type="submit">Speichern</button>
|
<button class="btn btn--primary" type="submit">Speichern</button>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{{ field('name', 'Firmenname', value=customer.name if customer else '', required=True) }}
|
{{ field('name', 'Firmenname', value=customer.name if customer else '', required=True) }}
|
||||||
{{ field('contact_email', 'E-Mail', value=customer.contact_email if customer else '',
|
{{ field('contact_email', 'E-Mail', value=customer.contact_email if customer else '',
|
||||||
type='email', required=not customer,
|
type='email', required=not customer,
|
||||||
hint_text='Pflicht bei der Neuanlage -- ueber diese Adresse laeuft das Kundenportal.') }}
|
hint_text='Pflicht bei der Neuanlage -- über diese Adresse läuft das Kundenportal.') }}
|
||||||
{{ field('contact_person', 'Ansprechpartner', value=customer.contact_person if customer else '') }}
|
{{ field('contact_person', 'Ansprechpartner', value=customer.contact_person if customer else '') }}
|
||||||
{{ field('contact_phone', 'Telefon', value=customer.contact_phone if customer else '') }}
|
{{ field('contact_phone', 'Telefon', value=customer.contact_phone if customer else '') }}
|
||||||
{{ field('street', 'Strasse', value=customer.street if customer else '') }}
|
{{ field('street', 'Strasse', value=customer.street if customer else '') }}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
{% block dialogs %}
|
{% block dialogs %}
|
||||||
{% if current_user.has('customers.create') %}
|
{% if current_user.has('customers.create') %}
|
||||||
{% call modal('customer-new', 'Kunde anlegen', 'Adresse in Einzelfeldern -- fuer Rechnungen weiterverwendbar', 'wide') %}
|
{% call modal('customer-new', 'Kunde anlegen', 'Adresse in Einzelfeldern -- für Rechnungen weiterverwendbar', 'wide') %}
|
||||||
<form method="post" action="{{ url_for('customers.create') }}">
|
<form method="post" action="{{ url_for('customers.create') }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="modal__body">{{ customer_form() }}</div>
|
<div class="modal__body">{{ customer_form() }}</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% extends "tesm_core/base.html" %}
|
||||||
{% from "tesm_core/macros.html" import pill, empty_state, alert, stat %}
|
{% from "tesm_core/macros.html" import pill, empty_state, alert, stat %}
|
||||||
|
|
||||||
{% block page_title %}Uebersicht{% endblock %}
|
{% block page_title %}Übersicht{% endblock %}
|
||||||
{% block page_sub %}
|
{% block page_sub %}
|
||||||
{% if summary %}
|
{% if summary %}
|
||||||
{{ summary.counts.customers }} Kunden · {{ summary.counts.licenses_active }} aktive Lizenzen
|
{{ summary.counts.customers }} Kunden · {{ summary.counts.licenses_active }} aktive Lizenzen
|
||||||
@@ -24,13 +24,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if summary.counts.tickets_blocked %}
|
{% if summary.counts.tickets_blocked %}
|
||||||
{{ alert(summary.counts.tickets_blocked ~ ' Ticket(s) sind gesperrt -- fuer diese Kunden
|
{{ alert(summary.counts.tickets_blocked ~ ' Ticket(s) sind gesperrt -- für diese Kunden
|
||||||
lassen sich keine neuen Lizenzen ausstellen.', 'warning') }}
|
lassen sich keine neuen Lizenzen ausstellen.', 'warning') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for key, label, kind, hint in [
|
{% for key, label, kind, hint in [
|
||||||
('red', 'Kritisch', 'danger', 'Weniger als 30 Tage Restlaufzeit oder bereits abgelaufen'),
|
('red', 'Kritisch', 'danger', 'Weniger als 30 Tage Restlaufzeit oder bereits abgelaufen'),
|
||||||
('orange', 'Bald faellig', 'warning', '30 bis 90 Tage Restlaufzeit'),
|
('orange', 'Bald fällig', 'warning', '30 bis 90 Tage Restlaufzeit'),
|
||||||
('green', 'In Ordnung', 'ok', 'Mehr als 90 Tage oder unbefristet')] %}
|
('green', 'In Ordnung', 'ok', 'Mehr als 90 Tage oder unbefristet')] %}
|
||||||
{% set entries = summary.buckets[key] %}
|
{% set entries = summary.buckets[key] %}
|
||||||
{% if entries %}
|
{% if entries %}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% if state.revision_pending %}
|
{% if state.revision_pending %}
|
||||||
{{ alert('Es wurde eine neue Revision signiert, die der Kunde noch nicht abgeholt hat.
|
{{ alert('Es wurde eine neue Revision signiert, die der Kunde noch nicht abgeholt hat.
|
||||||
Eine erreichbare Installation holt sie beim naechsten Heartbeat; eine Installation ohne
|
Eine erreichbare Installation holt sie beim nächsten Heartbeat; eine Installation ohne
|
||||||
Verbindung braucht dafuer den Offline-Weg -- der Kunde findet ihn in seinem Portal.',
|
Verbindung braucht dafür den Offline-Weg -- der Kunde findet ihn in seinem Portal.',
|
||||||
'warning', 'Revision ausstehend') }}
|
'warning', 'Revision ausstehend') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if license.status == 'revoked' %}
|
{% if license.status == 'revoked' %}
|
||||||
{{ alert('Diese Lizenz ist widerrufen. Der Client erfaehrt das beim naechsten Heartbeat und
|
{{ alert('Diese Lizenz ist widerrufen. Der Client erfährt das beim nächsten Heartbeat und
|
||||||
schaltet seine lizenzpflichtigen Funktionen ab.' ~ ((' Grund: ' ~ license.revoke_reason) if
|
schaltet seine lizenzpflichtigen Funktionen ab.' ~ ((' Grund: ' ~ license.revoke_reason) if
|
||||||
license.revoke_reason else ''), 'danger', 'Widerrufen') }}
|
license.revoke_reason else ''), 'danger', 'Widerrufen') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
{% else %}<span class="faint">keine</span>{% endif %}
|
{% else %}<span class="faint">keine</span>{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Ausgestellt</dt><dd>{{ license.issued_at | dt }} von {{ license.created_by or '-' }}</dd>
|
<dt>Ausgestellt</dt><dd>{{ license.issued_at | dt }} von {{ license.created_by or '-' }}</dd>
|
||||||
<dt>Laeuft ab</dt>
|
<dt>Läuft ab</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if license.lifetime %}{{ pill('unbefristet', 'online') }}
|
{% if license.lifetime %}{{ pill('unbefristet', 'online') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -105,9 +105,9 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('refresh', size=17) }} Nachtraeglich anpassen</div>
|
<div class="card__title">{{ icon('refresh', size=17) }} Nachträglich anpassen</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Signiert eine neue Revision. Der Client erhaelt sie beim naechsten Heartbeat --
|
Signiert eine neue Revision. Der Client erhält sie beim nächsten Heartbeat --
|
||||||
ohne erneute Aktivierung.
|
ohne erneute Aktivierung.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,9 +126,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ field('extend_days', 'Laufzeit verlaengern (Tage)', type='number',
|
{{ field('extend_days', 'Laufzeit verlängern (Tage)', type='number',
|
||||||
placeholder='0',
|
placeholder='0',
|
||||||
hint_text='Wird auf das aktuelle Ablaufdatum aufgeschlagen.') }}
|
hint_text='Wird auf das aktülle Ablaufdatum aufgeschlagen.') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
<button class="btn btn--primary" type="submit">Neue Revision signieren</button>
|
<button class="btn btn--primary" type="submit">Neue Revision signieren</button>
|
||||||
@@ -141,13 +141,13 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('transfer', size=17) }} Zustellungen</div>
|
<div class="card__title">{{ icon('transfer', size=17) }} Zustellungen</div>
|
||||||
<div class="card__sub">{{ deliveries | length }} Vorgaenge</div>
|
<div class="card__sub">{{ deliveries | length }} Vorgänge</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if deliveries %}
|
{% if deliveries %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>Zeit</th><th>Weg</th><th>Empfaenger</th><th>Durch</th><th>Detail</th></tr></thead>
|
<thead><tr><th>Zeit</th><th>Weg</th><th>Empfänger</th><th>Durch</th><th>Detail</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in deliveries %}
|
{% for entry in deliveries %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header" style="cursor:pointer">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('file-text', size=17) }} Signiertes Dokument</div>
|
<div class="card__title">{{ icon('file-text', size=17) }} Signiertes Dokument</div>
|
||||||
<div class="card__sub">Genau so, wie es signiert wurde -- jede Aenderung bricht die Signatur</div>
|
<div class="card__sub">Genau so, wie es signiert wurde -- jede Änderung bricht die Signatur</div>
|
||||||
</div>
|
</div>
|
||||||
{{ icon('chevron-down', size=16) }}
|
{{ icon('chevron-down', size=16) }}
|
||||||
</summary>
|
</summary>
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('mail', size=17) }} Per E-Mail versenden</div>
|
<div class="card__title">{{ icon('mail', size=17) }} Per E-Mail versenden</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
{% if mail_ready %}Ueber Microsoft Graph{% else %}Nicht konfiguriert{% endif %}
|
{% if mail_ready %}Über Microsoft Graph{% else %}Nicht konfiguriert{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
data-no-guard>
|
data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
{{ field('recipient', 'Empfaenger', value=license.contact_email, type='email',
|
{{ field('recipient', 'Empfänger', value=license.contact_email, type='email',
|
||||||
disabled=not mail_ready) }}
|
disabled=not mail_ready) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
data-dialog-open="revoke-dialog">Widerrufen</button>
|
data-dialog-open="revoke-dialog">Widerrufen</button>
|
||||||
<p class="fs-xs faint">
|
<p class="fs-xs faint">
|
||||||
Das Ticket bleibt offen -- eine neue Lizenz kann sofort ausgestellt werden. Genau so
|
Das Ticket bleibt offen -- eine neue Lizenz kann sofort ausgestellt werden. Genau so
|
||||||
laeuft ein Systemwechsel.
|
läuft ein Systemwechsel.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.has('licenses.delete') %}
|
{% if current_user.has('licenses.delete') %}
|
||||||
@@ -225,9 +225,9 @@
|
|||||||
<button class="btn btn--sm btn--ghost danger-text btn--block" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text btn--block" type="submit"
|
||||||
{% if license.status not in ('revoked', 'issued') %}disabled
|
{% if license.status not in ('revoked', 'issued') %}disabled
|
||||||
title="Eine aktive Lizenz muss erst widerrufen werden"{% endif %}
|
title="Eine aktive Lizenz muss erst widerrufen werden"{% endif %}
|
||||||
data-confirm="Lizenz endgueltig aus der Datenbank entfernen? Die Historie geht verloren."
|
data-confirm="Lizenz endgültig aus der Datenbank entfernen? Die Historie geht verloren."
|
||||||
data-confirm-title="Lizenz loeschen" data-confirm-ok="Loeschen">
|
data-confirm-title="Lizenz löschen" data-confirm-ok="Löschen">
|
||||||
Endgueltig loeschen
|
Endgültig löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
<form method="post" action="{{ url_for('licenses.revoke', license_id=license.license_id) }}">
|
<form method="post" action="{{ url_for('licenses.revoke', license_id=license.license_id) }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="modal__body">
|
<div class="modal__body">
|
||||||
{{ alert('Der Client erfaehrt den Widerruf beim naechsten Heartbeat und schaltet
|
{{ alert('Der Client erfährt den Widerruf beim nächsten Heartbeat und schaltet
|
||||||
lizenzpflichtige Funktionen ab. Das Ticket bleibt offen.', 'warning') }}
|
lizenzpflichtige Funktionen ab. Das Ticket bleibt offen.', 'warning') }}
|
||||||
{{ field('reason', 'Grund (erscheint im Protokoll)', placeholder='Systemwechsel') }}
|
{{ field('reason', 'Grund (erscheint im Protokoll)', placeholder='Systemwechsel') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
{% from "tesm_core/macros.html" import alert %}
|
{% from "tesm_core/macros.html" import alert %}
|
||||||
|
|
||||||
{% block page_title %}Offline-Aktivierung{% endblock %}
|
{% block page_title %}Offline-Aktivierung{% endblock %}
|
||||||
{% block page_sub %}Code des Kunden verarbeiten und Antwortcode zurueckgeben{% endblock %}
|
{% block page_sub %}Code des Kunden verarbeiten und Antwortcode zurückgeben{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Beide Codes sind signierte Protokollobjekte -- inhaltlich identisch mit dem
|
{{ alert('Beide Codes sind signierte Protokollobjekte -- inhaltlich identisch mit dem
|
||||||
Online-Weg, nur als kopierbarer Text. Die Pruefungen (Signatur, Frische, Nonce, Fingerprint)
|
Online-Weg, nur als kopierbarer Text. Die Prüfungen (Signatur, Frische, Nonce, Fingerprint)
|
||||||
laufen exakt gleich; es gibt hier keine Abkuerzung.', 'info') }}
|
laufen exakt gleich; es gibt hier keine Abkürzung.', 'info') }}
|
||||||
|
|
||||||
<div class="grid grid--split">
|
<div class="grid grid--split">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -20,8 +20,8 @@ laufen exakt gleich; es gibt hier keine Abkuerzung.', 'info') }}
|
|||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">Anfragecode</span>
|
<span class="field__label">Anfragecode</span>
|
||||||
<textarea class="textarea input--mono" name="code" rows="8" required
|
<textarea class="textarea input--mono" name="code" rows="8" required
|
||||||
placeholder="Vom Kunden erhaltenen Code einfuegen ..."></textarea>
|
placeholder="Vom Kunden erhaltenen Code einfügen ..."></textarea>
|
||||||
<span class="field__hint">Umbrueche und Leerzeichen sind erlaubt.</span>
|
<span class="field__hint">Umbrüche und Leerzeichen sind erlaubt.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
{% block page_sub %}
|
{% block page_sub %}
|
||||||
{% if state and state.valid and state.activated %}
|
{% if state and state.valid and state.activated %}
|
||||||
aktiv · Rolle {{ role_labels.get(state.role, state.role) }}
|
aktiv · Rolle {{ role_labels.get(state.role, state.role) }}
|
||||||
{% elif state and state.valid %}gueltig, nicht aktiviert
|
{% elif state and state.valid %}gültig, nicht aktiviert
|
||||||
{% elif state and state.present %}ungueltig
|
{% elif state and state.present %}ungültig
|
||||||
{% else %}keine Lizenz installiert{% endif %}
|
{% else %}keine Lizenz installiert{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block topbar_extra %}
|
{% block topbar_extra %}
|
||||||
<a class="btn btn--sm" href="{{ url_for('own_license.signing_key_info') }}">
|
<a class="btn btn--sm" href="{{ url_for('own_license.signing_key_info') }}">
|
||||||
{{ icon('key', size=15) }} Signaturschluessel
|
{{ icon('key', size=15) }} Signaturschlüssel
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -23,14 +23,14 @@
|
|||||||
{% if state and state.error %}{{ alert(state.error, 'danger') }}{% endif %}
|
{% if state and state.error %}{{ alert(state.error, 'danger') }}{% endif %}
|
||||||
|
|
||||||
{% if state and state.role == 'master' %}
|
{% if state and state.role == 'master' %}
|
||||||
{{ alert('Diese Instanz ist die Wurzelinstanz. Nur sie darf Lizenzen fuer
|
{{ alert('Diese Instanz ist die Wurzelinstanz. Nur sie darf Lizenzen für
|
||||||
weitere Lizenzserver ausstellen -- diese Grenze haengt an der signierten Rolle dieser
|
weitere Lizenzserver ausstellen -- diese Grenze hängt an der signierten Rolle dieser
|
||||||
Lizenzdatei, nicht an einem Gruppenrecht. Ein lokaler Administrator eines Sub-Lizenzservers
|
Lizenzdatei, nicht an einem Gruppenrecht. Ein lokaler Administrator eines Sub-Lizenzservers
|
||||||
kann sie sich damit nicht selbst einraeumen.', 'info', 'Rolle: Master') }}
|
kann sie sich damit nicht selbst einräumen.', 'info', 'Rolle: Master') }}
|
||||||
{% elif state and state.role == 'license_server' %}
|
{% elif state and state.role == 'license_server' %}
|
||||||
{{ alert('Diese Instanz ist ein Sub-Lizenzserver. Sie darf eigene Kunden,
|
{{ alert('Diese Instanz ist ein Sub-Lizenzserver. Sie darf eigene Kunden,
|
||||||
Tickets und Lizenzen verwalten -- aber keine weiteren Lizenzserver-Lizenzen ausstellen. Der
|
Tickets und Lizenzen verwalten -- aber keine weiteren Lizenzserver-Lizenzen ausstellen. Der
|
||||||
eigene Bestand wird automatisch an den uebergeordneten Master gemeldet.', 'info',
|
eigene Bestand wird automatisch an den übergeordneten Master gemeldet.', 'info',
|
||||||
'Rolle: Lizenzserver') }}
|
'Rolle: Lizenzserver') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<dt>Inhaber</dt><dd>{{ state.customer_name or '–' | safe }}</dd>
|
<dt>Inhaber</dt><dd>{{ state.customer_name or '–' | safe }}</dd>
|
||||||
<dt>Lizenz-ID</dt><dd class="mono fs-xs break-all">{{ state.status.license_id }}</dd>
|
<dt>Lizenz-ID</dt><dd class="mono fs-xs break-all">{{ state.status.license_id }}</dd>
|
||||||
<dt>Revision</dt><dd>{{ state.status.seq }}</dd>
|
<dt>Revision</dt><dd>{{ state.status.seq }}</dd>
|
||||||
<dt>Laeuft ab</dt>
|
<dt>Läuft ab</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if state.status.lifetime %}{{ pill('unbefristet', 'online') }}
|
{% if state.status.lifetime %}{{ pill('unbefristet', 'online') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{{ pill('abgelaufen', 'danger') }}
|
{{ pill('abgelaufen', 'danger') }}
|
||||||
{% if state.status.grace_active %}
|
{% if state.status.grace_active %}
|
||||||
<div class="fs-xs warning-text">
|
<div class="fs-xs warning-text">
|
||||||
Kulanzfrist laeuft ({{ grace_days }} Tage).
|
Kulanzfrist läuft ({{ grace_days }} Tage).
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif state.status.expiring_soon %}
|
{% elif state.status.expiring_soon %}
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Uebergeordneter Server</dt>
|
<dt>Übergeordneter Server</dt>
|
||||||
<dd class="break-all">
|
<dd class="break-all">
|
||||||
{{ state.status.master_endpoint or 'keiner (Wurzelinstanz oder Offline-Betrieb)' }}
|
{{ state.status.master_endpoint or 'keiner (Wurzelinstanz oder Offline-Betrieb)' }}
|
||||||
</dd>
|
</dd>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ empty_state('Keine gueltige Lizenz',
|
{{ empty_state('Keine gültige Lizenz',
|
||||||
'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap"
|
'Spielen Sie eine Lizenzdatei ein oder erzeugen Sie mit "tesm-license-admin bootstrap"
|
||||||
die eigene Lizenz der Wurzelinstanz.', 'key') }}
|
die eigene Lizenz der Wurzelinstanz.', 'key') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -133,12 +133,12 @@
|
|||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">Antwortcode des uebergeordneten Servers</span>
|
<span class="field__label">Antwortcode des übergeordneten Servers</span>
|
||||||
<textarea class="textarea input--mono" name="code" rows="4" required></textarea>
|
<textarea class="textarea input--mono" name="code" rows="4" required></textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
<button class="btn btn--primary" type="submit">Antwort uebernehmen</button>
|
<button class="btn btn--primary" type="submit">Antwort übernehmen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('upload', size=17) }} Lizenzdatei einspielen</div>
|
<div class="card__title">{{ icon('upload', size=17) }} Lizenzdatei einspielen</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Fuer einen Sub-Lizenzserver: die Datei, die der Master ausgestellt hat
|
Für einen Sub-Lizenzserver: die Datei, die der Master ausgestellt hat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
<input class="input" type="file" name="license_file" accept=".json,.lic" required>
|
<input class="input" type="file" name="license_file" accept=".json,.lic" required>
|
||||||
{% if trusted_keys and current_user.is_admin %}
|
{% if trusted_keys and current_user.is_admin %}
|
||||||
{{ checkbox('allow_new_issuer', 'Wechsel des Ausstellers ausdruecklich zulassen') }}
|
{{ checkbox('allow_new_issuer', 'Wechsel des Ausstellers ausdrücklich zulassen') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
<dt>Hostname</dt><dd class="mono">{{ hostname }}</dd>
|
<dt>Hostname</dt><dd class="mono">{{ hostname }}</dd>
|
||||||
<dt>Fingerprint</dt><dd class="mono fs-xs break-all">{{ fingerprint }}</dd>
|
<dt>Fingerprint</dt><dd class="mono fs-xs break-all">{{ fingerprint }}</dd>
|
||||||
<dt>Signaturschluessel</dt>
|
<dt>Signaturschlüssel</dt>
|
||||||
<dd class="mono">{{ signing_key_health.get('key_id') or 'fehlt' }}</dd>
|
<dd class="mono">{{ signing_key_health.get('key_id') or 'fehlt' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,8 +191,8 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('shield', size=17) }} Vertrauenswuerdige Aussteller</div>
|
<div class="card__title">{{ icon('shield', size=17) }} Vertrauenswürdige Aussteller</div>
|
||||||
<div class="card__sub">{{ trusted_keys | length }} gepinnte Schluessel</div>
|
<div class="card__sub">{{ trusted_keys | length }} gepinnte Schlüssel</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('shield', size=17) }} Ihre Lizenzen</div>
|
<div class="card__title">{{ icon('shield', size=17) }} Ihre Lizenzen</div>
|
||||||
<div class="card__sub">{{ tickets | length }} Vertrag/Vertraege</div>
|
<div class="card__sub">{{ tickets | length }} Vertrag/Verträge</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body stack stack--tight">
|
<div class="card__body stack stack--tight">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
{{ field('contact_person', 'Ansprechpartner', value=customer.contact_person) }}
|
{{ field('contact_person', 'Ansprechpartner', value=customer.contact_person) }}
|
||||||
{{ field('contact_phone', 'Telefon', value=customer.contact_phone) }}
|
{{ field('contact_phone', 'Telefon', value=customer.contact_phone) }}
|
||||||
{{ field('contact_email', 'E-Mail', value=customer.contact_email, type='email',
|
{{ field('contact_email', 'E-Mail', value=customer.contact_email, type='email',
|
||||||
hint_text='Ueber diese Adresse laeuft der Zugang zu diesem Portal.') }}
|
hint_text='Über diese Adresse läuft der Zugang zu diesem Portal.') }}
|
||||||
{{ field('street', 'Strasse', value=customer.street) }}
|
{{ field('street', 'Strasse', value=customer.street) }}
|
||||||
{{ field('house_number', 'Hausnummer', value=customer.house_number) }}
|
{{ field('house_number', 'Hausnummer', value=customer.house_number) }}
|
||||||
{{ field('postal_code', 'PLZ', value=customer.postal_code) }}
|
{{ field('postal_code', 'PLZ', value=customer.postal_code) }}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<p class="fs-sm muted center">
|
<p class="fs-sm muted center">
|
||||||
Geben Sie die bei uns hinterlegte E-Mail-Adresse ein. Sie erhalten einen Link, der
|
Geben Sie die bei uns hinterlegte E-Mail-Adresse ein. Sie erhalten einen Link, der
|
||||||
{{ minutes }} Minuten gueltig ist und sich einmal verwenden laesst.
|
{{ minutes }} Minuten gültig ist und sich einmal verwenden lässt.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="post" class="stack" data-no-guard>
|
<form method="post" class="stack" data-no-guard>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="auth-footnote">
|
<p class="auth-footnote">
|
||||||
Aus Datenschutzgruenden bekommen Sie immer dieselbe Rueckmeldung -- unabhaengig davon, ob die
|
Aus Datenschutzgründen bekommen Sie immer dieselbe Rückmeldung -- unabhängig davon, ob die
|
||||||
Adresse bei uns bekannt ist.
|
Adresse bei uns bekannt ist.
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Diese Daten verwaltet der Sub-Lizenzserver selbst. Sie sind hier rein informativ und
|
{{ alert('Diese Daten verwaltet der Sub-Lizenzserver selbst. Sie sind hier rein informativ und
|
||||||
werden bei jedem Bericht vollstaendig ersetzt.', 'info') }}
|
werden bei jedem Bericht vollständig ersetzt.', 'info') }}
|
||||||
|
|
||||||
{% if reported.customers %}
|
{% if reported.customers %}
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
@@ -71,7 +71,7 @@ werden bei jedem Bericht vollstaendig ersetzt.', 'info') }}
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ empty_state('Noch kein Bericht',
|
{{ empty_state('Noch kein Bericht',
|
||||||
'Dieser Lizenzserver hat noch nichts gemeldet. Der erste Bericht kommt mit dem naechsten
|
'Dieser Lizenzserver hat noch nichts gemeldet. Der erste Bericht kommt mit dem nächsten
|
||||||
Heartbeat.', 'share') }}
|
Heartbeat.', 'share') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Ein Sub-Lizenzserver meldet seinen kompletten Bestand -- huckepack auf dem
|
{{ alert('Ein Sub-Lizenzserver meldet seinen kompletten Bestand -- huckepack auf dem
|
||||||
' ~ heartbeat_hours ~ '-Stunden-Heartbeat und zusaetzlich sofort nach jeder Aenderung. Jeder
|
' ~ heartbeat_hours ~ '-Stunden-Heartbeat und zusätzlich sofort nach jeder Änderung. Jeder
|
||||||
Bericht ist ein vollstaendiger Schnappschuss: Geloeschtes verschwindet automatisch auch hier.',
|
Bericht ist ein vollständiger Schnappschuss: Gelöschtes verschwindet automatisch auch hier.',
|
||||||
'info') }}
|
'info') }}
|
||||||
|
|
||||||
{% if sources %}
|
{% if sources %}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">
|
<div class="card__title">
|
||||||
{{ icon('shield', size=17) }} Aktuelle Lizenz
|
{{ icon('shield', size=17) }} Aktülle Lizenz
|
||||||
{% if current.status == 'active' %}{{ pill('aktiv', 'online') }}
|
{% if current.status == 'active' %}{{ pill('aktiv', 'online') }}
|
||||||
{% else %}{{ pill('bereit zum Einspielen', 'info') }}{% endif %}
|
{% else %}{{ pill('bereit zum Einspielen', 'info') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
<dt>Gueltig bis</dt>
|
<dt>Gültig bis</dt>
|
||||||
<dd>{% if current.lifetime %}unbefristet{% else %}{{ current.expires_at | date }}{% endif %}</dd>
|
<dd>{% if current.lifetime %}unbefristet{% else %}{{ current.expires_at | date }}{% endif %}</dd>
|
||||||
{% if current.hostname %}<dt>Installiert auf</dt><dd>{{ current.hostname }}</dd>{% endif %}
|
{% if current.hostname %}<dt>Installiert auf</dt><dd>{{ current.hostname }}</dd>{% endif %}
|
||||||
<dt>Ausgestellt</dt><dd>{{ current.issued_at | date }}</dd>
|
<dt>Ausgestellt</dt><dd>{{ current.issued_at | date }}</dd>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
action="{{ url_for('selfservice.revoke', ticket_id=ticket.id, license_id=current.license_id) }}">
|
action="{{ url_for('selfservice.revoke', ticket_id=ticket.id, license_id=current.license_id) }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--ghost danger-text btn--block" type="submit"
|
<button class="btn btn--ghost danger-text btn--block" type="submit"
|
||||||
data-confirm="Lizenz freigeben? Ihre bisherige Installation verliert damit die lizenzpflichtigen Funktionen. Danach koennen Sie sofort eine neue Lizenz fuer Ihr neues System ausstellen."
|
data-confirm="Lizenz freigeben? Ihre bisherige Installation verliert damit die lizenzpflichtigen Funktionen. Danach können Sie sofort eine neue Lizenz für Ihr neues System ausstellen."
|
||||||
data-confirm-title="Lizenz freigeben" data-confirm-ok="Freigeben">
|
data-confirm-title="Lizenz freigeben" data-confirm-ok="Freigeben">
|
||||||
Lizenz freigeben (Systemwechsel)
|
Lizenz freigeben (Systemwechsel)
|
||||||
</button>
|
</button>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('shield', size=17) }} Noch keine Lizenz</div>
|
<div class="card__title">{{ icon('shield', size=17) }} Noch keine Lizenz</div>
|
||||||
<div class="card__sub">Sie koennen Ihre Lizenz jetzt erstellen</div>
|
<div class="card__sub">Sie können Ihre Lizenz jetzt erstellen</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
@@ -101,8 +101,8 @@
|
|||||||
<details class="card">
|
<details class="card">
|
||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header" style="cursor:pointer">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('history', size=17) }} Frueher ausgestellt</div>
|
<div class="card__title">{{ icon('history', size=17) }} Früher ausgestellt</div>
|
||||||
<div class="card__sub">{{ history | length }} Eintraege</div>
|
<div class="card__sub">{{ history | length }} Einträge</div>
|
||||||
</div>
|
</div>
|
||||||
{{ icon('chevron-down', size=16) }}
|
{{ icon('chevron-down', size=16) }}
|
||||||
</summary>
|
</summary>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<td class="fs-sm">{{ entry.issued_at | date }}</td>
|
<td class="fs-sm">{{ entry.issued_at | date }}</td>
|
||||||
<td class="fs-sm">
|
<td class="fs-sm">
|
||||||
{% if entry.status == 'revoked' %}freigegeben
|
{% if entry.status == 'revoked' %}freigegeben
|
||||||
{% elif entry.status == 'deactivated' %}abgeloest
|
{% elif entry.status == 'deactivated' %}abgelöst
|
||||||
{% else %}{{ entry.status }}{% endif %}
|
{% else %}{{ entry.status }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-sm">{{ entry.hostname or '–' | safe }}</td>
|
<td class="fs-sm">{{ entry.hostname or '–' | safe }}</td>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{{ alert('In Ihrer TESM-Installation unter Verwaltung -- Lizenz auf "Lizenz aktivieren"
|
{{ alert('In Ihrer TESM-Installation unter Verwaltung -- Lizenz auf "Lizenz aktivieren"
|
||||||
klicken. Erscheint dort ein Anfragecode, fuegen Sie ihn hier ein. Sie bekommen einen
|
klicken. Erscheint dort ein Anfragecode, fügen Sie ihn hier ein. Sie bekommen einen
|
||||||
Antwortcode zurueck, den Sie in TESM eintragen. Beide Codes sind signiert und lassen
|
Antwortcode zurück, den Sie in TESM eintragen. Beide Codes sind signiert und lassen
|
||||||
sich unterwegs nicht veraendern.', 'info', 'So geht es') }}
|
sich unterwegs nicht verändern.', 'info', 'So geht es') }}
|
||||||
|
|
||||||
{% if response_code %}
|
{% if response_code %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">Anfragecode aus Ihrer TESM-Installation</span>
|
<span class="field__label">Anfragecode aus Ihrer TESM-Installation</span>
|
||||||
<textarea class="textarea input--mono" name="code" rows="6" required
|
<textarea class="textarea input--mono" name="code" rows="6" required
|
||||||
placeholder="Code einfuegen ..."></textarea>
|
placeholder="Code einfügen ..."></textarea>
|
||||||
</label>
|
</label>
|
||||||
<div class="row row--end mt-3">
|
<div class="row row--end mt-3">
|
||||||
<button class="btn btn--primary" type="submit">Antwortcode erzeugen</button>
|
<button class="btn btn--primary" type="submit">Antwortcode erzeugen</button>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="fs-xs faint">
|
<p class="fs-xs faint">
|
||||||
<a href="{{ url_for('selfservice.overview', ticket_id=ticket.id) }}">Zurueck zur Uebersicht</a>
|
<a href="{{ url_for('selfservice.overview', ticket_id=ticket.id) }}">Zurück zur Übersicht</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% block page_title %}E-Mail-Versand{% endblock %}
|
{% block page_title %}E-Mail-Versand{% endblock %}
|
||||||
{% block page_sub %}
|
{% block page_sub %}
|
||||||
Microsoft Graph (App-only) ·
|
Microsoft Graph (App-only) ·
|
||||||
{% if ready %}einsatzbereit{% else %}nicht vollstaendig konfiguriert{% endif %}
|
{% if ready %}einsatzbereit{% else %}nicht vollständig konfiguriert{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block topbar_extra %}
|
{% block topbar_extra %}
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Der Versand laeuft ueber den Client-Credentials-Flow von Microsoft Graph -- kein SMTP,
|
{{ alert('Der Versand läuft über den Client-Credentials-Flow von Microsoft Graph -- kein SMTP,
|
||||||
kein zusaetzliches Paket. Die Anwendung braucht in Azure die Anwendungsberechtigung
|
kein zusätzliches Paket. Die Anwendung braucht in Azure die Anwendungsberechtigung
|
||||||
Mail.Send. Der Verbindungstest holt nur ein Token und verschickt
|
Mail.Send. Der Verbindungstest holt nur ein Token und verschickt
|
||||||
bewusst keine echte Nachricht.', 'info') }}
|
bewusst keine echte Nachricht.', 'info') }}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ bewusst keine echte Nachricht.', 'info') }}
|
|||||||
</div>
|
</div>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend>Client-Secret {% if has_secret %}{{ pill('gesetzt', 'online') }}{% else %}{{ pill('fehlt', 'warning') }}{% endif %}</legend>
|
<legend>Client-Secret {% if has_secret %}{{ pill('gesetzt', 'online') }}{% else %}{{ pill('fehlt', 'warning') }}{% endif %}</legend>
|
||||||
{{ field('client_secret', 'Neues Secret (leer = unveraendert)', type='password',
|
{{ field('client_secret', 'Neues Secret (leer = unverändert)', type='password',
|
||||||
autocomplete='new-password', disabled=not can_secrets) }}
|
autocomplete='new-password', disabled=not can_secrets) }}
|
||||||
{% if not can_secrets %}
|
{% if not can_secrets %}
|
||||||
<p class="fs-xs faint mt-2">
|
<p class="fs-xs faint mt-2">
|
||||||
@@ -63,17 +63,17 @@ bewusst keine echte Nachricht.', 'info') }}
|
|||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1"><div class="card__title">{{ icon('info', size=17) }} Wofuer der Versand genutzt wird</div></div>
|
<div class="flex-1"><div class="card__title">{{ icon('info', size=17) }} Wofür der Versand genutzt wird</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<ul class="stack stack--tight fs-sm muted">
|
<ul class="stack stack--tight fs-sm muted">
|
||||||
<li>Lizenzdatei an den Kunden senden (mit Anhang).</li>
|
<li>Lizenzdatei an den Kunden senden (mit Anhang).</li>
|
||||||
<li>Zugangslink fuer das Kundenportal.</li>
|
<li>Zugangslink für das Kundenportal.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<p class="fs-sm muted">
|
<p class="fs-sm muted">
|
||||||
Das Secret liegt AES-256-GCM-verschluesselt in der Datenbank, gebunden an seinen
|
Das Secret liegt AES-256-GCM-verschlüsselt in der Datenbank, gebunden an seinen
|
||||||
Verwendungszweck. Ohne die Schluesseldatei dieser Installation ist es nicht lesbar.
|
Verwendungszweck. Ohne die Schlüsseldatei dieser Installation ist es nicht lesbar.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if not sysops_available %}
|
{% if not sysops_available %}
|
||||||
{{ alert('Systemhelfer nicht verfuegbar -- Hostname und Zeitzone koennen hier nur angezeigt,
|
{{ alert('Systemhelfer nicht verfügbar -- Hostname und Zeitzone können hier nur angezeigt,
|
||||||
nicht geaendert werden.', 'info') }}
|
nicht geändert werden.', 'info') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="grid grid--split">
|
<div class="grid grid--split">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('users', size=17) }} Kundenportal und Self-Service</div>
|
<div class="card__title">{{ icon('users', size=17) }} Kundenportal und Self-Service</div>
|
||||||
<div class="card__sub">Zwei unabhaengige Zeitfenster: Link und Sitzung</div>
|
<div class="card__sub">Zwei unabhängige Zeitfenster: Link und Sitzung</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="{{ url_for('settings.system_save') }}">
|
<form method="post" action="{{ url_for('settings.system_save') }}">
|
||||||
@@ -24,10 +24,10 @@
|
|||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{{ checkbox('portal_enabled', 'Kundenportal aktiv', checked=values.portal_enabled) }}
|
{{ checkbox('portal_enabled', 'Kundenportal aktiv', checked=values.portal_enabled) }}
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{{ field('portal_link_minutes', 'Zugangslink gueltig (Minuten)',
|
{{ field('portal_link_minutes', 'Zugangslink gültig (Minuten)',
|
||||||
value=values.portal_link_minutes, type='number',
|
value=values.portal_link_minutes, type='number',
|
||||||
hint_text=specs['portal_link_minutes'].hint) }}
|
hint_text=specs['portal_link_minutes'].hint) }}
|
||||||
{{ field('portal_session_minutes', 'Portalsitzung gueltig (Minuten)',
|
{{ field('portal_session_minutes', 'Portalsitzung gültig (Minuten)',
|
||||||
value=values.portal_session_minutes, type='number',
|
value=values.portal_session_minutes, type='number',
|
||||||
hint_text=specs['portal_session_minutes'].hint) }}
|
hint_text=specs['portal_session_minutes'].hint) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
{{ checkbox('self_service_enabled', 'Self-Service-Link je Ticket aktiv',
|
{{ checkbox('self_service_enabled', 'Self-Service-Link je Ticket aktiv',
|
||||||
checked=values.self_service_enabled,
|
checked=values.self_service_enabled,
|
||||||
hint_text=specs['self_service_enabled'].hint) }}
|
hint_text=specs['self_service_enabled'].hint) }}
|
||||||
{{ checkbox('allow_customer_revoke', 'Kunden duerfen ihre Lizenz selbst freigeben',
|
{{ checkbox('allow_customer_revoke', 'Kunden dürfen ihre Lizenz selbst freigeben',
|
||||||
checked=values.allow_customer_revoke,
|
checked=values.allow_customer_revoke,
|
||||||
hint_text=specs['allow_customer_revoke'].hint) }}
|
hint_text=specs['allow_customer_revoke'].hint) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -60,6 +60,9 @@
|
|||||||
selected=values.log_rotate_interval) }}
|
selected=values.log_rotate_interval) }}
|
||||||
{{ field('log_rotate_keep', 'Aufbewahrte Rotationen', value=values.log_rotate_keep,
|
{{ field('log_rotate_keep', 'Aufbewahrte Rotationen', value=values.log_rotate_keep,
|
||||||
type='number') }}
|
type='number') }}
|
||||||
|
{{ field('session_idle_minutes', 'Automatische Abmeldung (Minuten)',
|
||||||
|
value=values.session_idle_minutes, type='number',
|
||||||
|
hint_text=specs['session_idle_minutes'].hint) }}
|
||||||
{{ field('trash_retention_days', 'Papierkorb-Aufbewahrung (Tage)',
|
{{ field('trash_retention_days', 'Papierkorb-Aufbewahrung (Tage)',
|
||||||
value=values.trash_retention_days, type='number') }}
|
value=values.trash_retention_days, type='number') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +108,7 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('sliders', size=17) }} Navigationsreihenfolge</div>
|
<div class="card__title">{{ icon('sliders', size=17) }} Navigationsreihenfolge</div>
|
||||||
<div class="card__sub">Gilt fuer alle Benutzer</div>
|
<div class="card__sub">Gilt für alle Benutzer</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="{{ url_for('admin.nav_order') }}">
|
<form method="post" action="{{ url_for('admin.nav_order') }}">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Diese Angaben werden in jede Lizenzdatei eingebettet und beim Kunden auf der
|
{{ alert('Diese Angaben werden in jede Lizenzdatei eingebettet und beim Kunden auf der
|
||||||
Lizenzseite angezeigt -- inklusive Logo. Der oeffentliche Endpunkt entscheidet zusaetzlich,
|
Lizenzseite angezeigt -- inklusive Logo. Der öffentliche Endpunkt entscheidet zusätzlich,
|
||||||
welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
||||||
|
|
||||||
<div class="grid grid--split">
|
<div class="grid grid--split">
|
||||||
@@ -26,18 +26,18 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
|||||||
disabled=not current_user.has('settings_vendor.edit')) }}
|
disabled=not current_user.has('settings_vendor.edit')) }}
|
||||||
</div>
|
</div>
|
||||||
{{ textarea_field('vendor_address', 'Adresse', value=values.vendor_address, rows=3) }}
|
{{ textarea_field('vendor_address', 'Adresse', value=values.vendor_address, rows=3) }}
|
||||||
{{ field('master_endpoint', 'Oeffentlicher Endpunkt', value=values.master_endpoint,
|
{{ field('master_endpoint', 'Öffentlicher Endpunkt', value=values.master_endpoint,
|
||||||
placeholder='https://lizenz.firma.de',
|
placeholder='https://lizenz.firma.de',
|
||||||
disabled=not current_user.has('settings_vendor.edit'),
|
disabled=not current_user.has('settings_vendor.edit'),
|
||||||
hint_text='Basis-URL, unter der Kundeninstanzen diesen Server erreichen. Wird in
|
hint_text='Basis-URL, unter der Kundeninstanzen diesen Server erreichen. Wird in
|
||||||
jede Lizenz eingebettet und fuer alle kundengerichteten Links verwendet -- statt
|
jede Lizenz eingebettet und für alle kundengerichteten Links verwendet -- statt
|
||||||
des Host-Headers der aktuellen Anfrage, der bei internen Aufrufen falsch waere.',
|
des Host-Headers der aktüllen Anfrage, der bei internen Aufrufen falsch wäre.',
|
||||||
extra_class='field--full') }}
|
extra_class='field--full') }}
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend>Logo (maximal {{ max_logo_kb }} KB)</legend>
|
<legend>Logo (maximal {{ max_logo_kb }} KB)</legend>
|
||||||
<div class="stack stack--tight">
|
<div class="stack stack--tight">
|
||||||
{% if values.vendor_logo_base64 %}
|
{% if values.vendor_logo_base64 %}
|
||||||
<img src="{{ values.vendor_logo_base64 }}" alt="Aktuelles Logo"
|
<img src="{{ values.vendor_logo_base64 }}" alt="Aktülles Logo"
|
||||||
style="max-height:64px;background:var(--surface-2);padding:var(--sp-2);border-radius:var(--r-sm)">
|
style="max-height:64px;background:var(--surface-2);padding:var(--sp-2);border-radius:var(--r-sm)">
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="faint fs-sm">Kein Logo hinterlegt.</p>
|
<p class="faint fs-sm">Kein Logo hinterlegt.</p>
|
||||||
@@ -45,7 +45,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
|||||||
<input class="input" type="file" name="vendor_logo" accept="image/png,image/jpeg,image/svg+xml"
|
<input class="input" type="file" name="vendor_logo" accept="image/png,image/jpeg,image/svg+xml"
|
||||||
{% if not current_user.has('settings_vendor.edit') %}disabled{% endif %}>
|
{% if not current_user.has('settings_vendor.edit') %}disabled{% endif %}>
|
||||||
<span class="field__hint">
|
<span class="field__hint">
|
||||||
Ein zu grosses Logo wird abgelehnt, die uebrigen Angaben werden trotzdem gespeichert.
|
Ein zu grosses Logo wird abgelehnt, die übrigen Angaben werden trotzdem gespeichert.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -75,7 +75,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
|||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<p class="fs-sm muted">
|
<p class="fs-sm muted">
|
||||||
Die Angaben werden zum Zeitpunkt der Ausstellung in die Lizenz kopiert und signiert. Eine
|
Die Angaben werden zum Zeitpunkt der Ausstellung in die Lizenz kopiert und signiert. Eine
|
||||||
Aenderung hier wirkt auf neue Lizenzen -- bestehende behalten ihren Stand, bis eine neue
|
Änderung hier wirkt auf neue Lizenzen -- bestehende behalten ihren Stand, bis eine neue
|
||||||
Revision signiert wird.
|
Revision signiert wird.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% extends "tesm_core/base.html" %}
|
||||||
{% from "tesm_core/macros.html" import alert %}
|
{% from "tesm_core/macros.html" import alert %}
|
||||||
{% block page_title %}Signaturschluessel{% endblock %}
|
{% block page_title %}Signaturschlüssel{% endblock %}
|
||||||
{% block page_sub %}Schluessel-ID {{ key_id }}{% endblock %}
|
{% block page_sub %}Schlüssel-ID {{ key_id }}{% endblock %}
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
<nav class="breadcrumb">
|
<nav class="breadcrumb">
|
||||||
<span><a href="{{ url_for('own_license.overview') }}">Eigene Lizenz</a></span><span>Signaturschluessel</span>
|
<span><a href="{{ url_for('own_license.overview') }}">Eigene Lizenz</a></span><span>Signaturschlüssel</span>
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Der oeffentliche Teil ist kein Geheimnis -- er reist in jeder ausgestellten Lizenz mit.
|
{{ alert('Der öffentliche Teil ist kein Geheimnis -- er reist in jeder ausgestellten Lizenz mit.
|
||||||
Ihn hier anzuzeigen erlaubt es, ihn unabhaengig zu vergleichen, bevor ein Kunde ihn als
|
Ihn hier anzuzeigen erlaubt es, ihn unabhängig zu vergleichen, bevor ein Kunde ihn als
|
||||||
vertrauenswuerdig hinterlegt.', 'info') }}
|
vertrauenswürdig hinterlegt.', 'info') }}
|
||||||
{% if health.problems %}
|
{% if health.problems %}
|
||||||
{% for problem in health.problems %}{{ alert(problem, 'danger') }}{% endfor %}
|
{% for problem in health.problems %}{{ alert(problem, 'danger') }}{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<section class="card" style="max-width:760px">
|
<section class="card" style="max-width:760px">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1"><div class="card__title">{{ icon('key', size=17) }} Oeffentlicher Schluessel</div>
|
<div class="flex-1"><div class="card__title">{{ icon('key', size=17) }} Öffentlicher Schlüssel</div>
|
||||||
<div class="card__sub">Erzeugt {{ created_at | dt }}</div></div>
|
<div class="card__sub">Erzeugt {{ created_at | dt }}</div></div>
|
||||||
<button class="btn btn--sm" type="button" data-copy="#pubkey">{{ icon('copy', size=14) }} Kopieren</button>
|
<button class="btn btn--sm" type="button" data-copy="#pubkey">{{ icon('copy', size=14) }} Kopieren</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
<dt>Schluessel-ID</dt><dd class="mono">{{ key_id }}</dd>
|
<dt>Schlüssel-ID</dt><dd class="mono">{{ key_id }}</dd>
|
||||||
<dt>Verfahren</dt><dd>Ed25519 (EdDSA)</dd>
|
<dt>Verfahren</dt><dd>Ed25519 (EdDSA)</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<pre class="code-block code-block--wrap" id="pubkey">{{ public_key }}</pre>
|
<pre class="code-block code-block--wrap" id="pubkey">{{ public_key }}</pre>
|
||||||
{{ alert('Der private Teil liegt ausschliesslich auf diesem Host (Modus 0600) und ist aus dem
|
{{ alert('Der private Teil liegt ausschliesslich auf diesem Host (Modus 0600) und ist aus dem
|
||||||
Deployment-Abgleich ausgeschlossen. Ginge er verloren, waere jede
|
Deployment-Abgleich ausgeschlossen. Ginge er verloren, wäre jede
|
||||||
ausgestellte Lizenz nicht mehr pruefbar -- bitte getrennt sichern.', 'warning') }}
|
ausgestellte Lizenz nicht mehr prüfbar -- bitte getrennt sichern.', 'warning') }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if endpoint_missing %}
|
{% if endpoint_missing %}
|
||||||
{{ alert(endpoint_missing, 'danger', 'Keine Ausstellung moeglich') }}
|
{{ alert(endpoint_missing, 'danger', 'Keine Ausstellung möglich') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ticket.status == 'blocked' %}
|
{% if ticket.status == 'blocked' %}
|
||||||
{{ alert('Dieses Ticket ist gesperrt. Es lassen sich keine neuen Lizenzen ausstellen -- weder
|
{{ alert('Dieses Ticket ist gesperrt. Es lassen sich keine neuen Lizenzen ausstellen -- weder
|
||||||
hier noch ueber den Self-Service des Kunden. Bereits ausgestellte Lizenzen bleiben gueltig.',
|
hier noch über den Self-Service des Kunden. Bereits ausgestellte Lizenzen bleiben gültig.',
|
||||||
'danger', 'Gesperrt') }}
|
'danger', 'Gesperrt') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<input type="hidden" name="as_trial" value="1">
|
<input type="hidden" name="as_trial" value="1">
|
||||||
<button class="btn" type="submit"
|
<button class="btn" type="submit"
|
||||||
{% if has_open_license or ticket.status == 'blocked' %}disabled{% endif %}
|
{% if has_open_license or ticket.status == 'blocked' %}disabled{% endif %}
|
||||||
data-confirm="Testlizenz ausstellen? {{ trial_days }} Tage Laufzeit mit allen Modulen. Das Ticket selbst bleibt unveraendert."
|
data-confirm="Testlizenz ausstellen? {{ trial_days }} Tage Laufzeit mit allen Modulen. Das Ticket selbst bleibt unverändert."
|
||||||
data-confirm-title="Testlizenz" data-confirm-ok="Ausstellen"
|
data-confirm-title="Testlizenz" data-confirm-ok="Ausstellen"
|
||||||
data-confirm-style="primary">
|
data-confirm-style="primary">
|
||||||
Als Testlizenz ({{ trial_days }} Tage, alle Module)
|
Als Testlizenz ({{ trial_days }} Tage, alle Module)
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
{% if licenses %}
|
{% if licenses %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>Lizenz</th><th>Ausgestellt</th><th>Zustand</th><th>Laeuft ab</th><th>Host</th></tr></thead>
|
<thead><tr><th>Lizenz</th><th>Ausgestellt</th><th>Zustand</th><th>Läuft ab</th><th>Host</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in licenses %}
|
{% for entry in licenses %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('link', size=17) }} Self-Service fuer den Kunden</div>
|
<div class="card__title">{{ icon('link', size=17) }} Self-Service für den Kunden</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Ohne Anmeldung nutzbar – die Berechtigung ist der Besitz dieses Links
|
Ohne Anmeldung nutzbar – die Berechtigung ist der Besitz dieses Links
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="fs-xs faint">
|
<p class="fs-xs faint">
|
||||||
Damit kann der Kunde seine Lizenz herunterladen, bei einem Systemwechsel freigeben und
|
Damit kann der Kunde seine Lizenz herunterladen, bei einem Systemwechsel freigeben und
|
||||||
neu ausstellen. Der Link enthaelt 192 Bit Zufall und ist nicht ratbar -- behandeln Sie
|
neu ausstellen. Der Link enthält 192 Bit Zufall und ist nicht ratbar -- behandeln Sie
|
||||||
ihn wie ein Passwort.
|
ihn wie ein Passwort.
|
||||||
</p>
|
</p>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% elif ticket.role != 'customer' %}
|
{% elif ticket.role != 'customer' %}
|
||||||
{{ alert('Fuer Lizenzserver-Tickets ist der Self-Service vollstaendig gesperrt -- diese
|
{{ alert('Für Lizenzserver-Tickets ist der Self-Service vollständig gesperrt -- diese
|
||||||
Lizenzen werden ausschliesslich hier verwaltet.', 'info') }}
|
Lizenzen werden ausschliesslich hier verwaltet.', 'info') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -158,8 +158,8 @@
|
|||||||
<form method="post" action="{{ url_for('tickets.update', ticket_id=ticket.id) }}">
|
<form method="post" action="{{ url_for('tickets.update', ticket_id=ticket.id) }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{{ alert('Aenderungen wirken auf die naechste Ausstellung. Kunde und Rolle sind
|
{{ alert('Änderungen wirken auf die nächste Ausstellung. Kunde und Rolle sind
|
||||||
unveraenderlich.', 'info') }}
|
unveränderlich.', 'info') }}
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">Lizenztyp</span>
|
<span class="field__label">Lizenztyp</span>
|
||||||
<select class="select" name="type" id="ticket-type"
|
<select class="select" name="type" id="ticket-type"
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
{% if current_user.has('tickets.edit') or current_user.has('tickets.delete') %}
|
{% if current_user.has('tickets.edit') or current_user.has('tickets.delete') %}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1"><div class="card__title">{{ icon('lock', size=17) }} Sperre und Loeschung</div></div>
|
<div class="flex-1"><div class="card__title">{{ icon('lock', size=17) }} Sperre und Löschung</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body stack stack--tight">
|
<div class="card__body stack stack--tight">
|
||||||
{% if current_user.has('tickets.edit') %}
|
{% if current_user.has('tickets.edit') %}
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
<button class="btn btn--sm btn--block {{ 'btn--danger' if ticket.status != 'blocked' }}"
|
<button class="btn btn--sm btn--block {{ 'btn--danger' if ticket.status != 'blocked' }}"
|
||||||
type="submit"
|
type="submit"
|
||||||
data-confirm="{% if ticket.status == 'blocked' %}Ticket entsperren?{% else %}Ticket sperren? Der Kunde kann danach keine neuen Lizenzen mehr ausstellen.{% endif %}"
|
data-confirm="{% if ticket.status == 'blocked' %}Ticket entsperren?{% else %}Ticket sperren? Der Kunde kann danach keine neuen Lizenzen mehr ausstellen.{% endif %}"
|
||||||
data-confirm-title="Ticketsperre" data-confirm-ok="Bestaetigen"
|
data-confirm-title="Ticketsperre" data-confirm-ok="Bestätigen"
|
||||||
data-confirm-style="{{ 'primary' if ticket.status == 'blocked' else 'danger' }}">
|
data-confirm-style="{{ 'primary' if ticket.status == 'blocked' else 'danger' }}">
|
||||||
{{ 'Entsperren' if ticket.status == 'blocked' else 'Sperren' }}
|
{{ 'Entsperren' if ticket.status == 'blocked' else 'Sperren' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -228,9 +228,9 @@
|
|||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text btn--block" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text btn--block" type="submit"
|
||||||
{% if has_open_license %}disabled title="Erst die offene Lizenz widerrufen"{% endif %}
|
{% if has_open_license %}disabled title="Erst die offene Lizenz widerrufen"{% endif %}
|
||||||
data-confirm="Ticket und seine gesamte Lizenzhistorie loeschen? Das laesst sich nicht rueckgaengig machen."
|
data-confirm="Ticket und seine gesamte Lizenzhistorie löschen? Das lässt sich nicht rückgängig machen."
|
||||||
data-confirm-title="Ticket loeschen" data-confirm-ok="Loeschen">
|
data-confirm-title="Ticket löschen" data-confirm-ok="Löschen">
|
||||||
Ticket loeschen
|
Ticket löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">Kunde <span class="req">*</span></span>
|
<span class="field__label">Kunde <span class="req">*</span></span>
|
||||||
<select class="select" name="customer_id" required>
|
<select class="select" name="customer_id" required>
|
||||||
<option value="">bitte waehlen</option>
|
<option value="">bitte wählen</option>
|
||||||
{% for customer in customers %}
|
{% for customer in customers %}
|
||||||
<option value="{{ customer.id }}"
|
<option value="{{ customer.id }}"
|
||||||
{% if preselected_customer == customer.id %}selected{% endif %}>
|
{% if preselected_customer == customer.id %}selected{% endif %}>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<span class="field__hint">
|
<span class="field__hint">
|
||||||
Der Kunde ist nach dem Anlegen unveraenderlich -- der Self-Service-Link ist an ihn
|
Der Kunde ist nach dem Anlegen unveränderlich -- der Self-Service-Link ist an ihn
|
||||||
gebunden.
|
gebunden.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -47,13 +47,13 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<span class="field__hint">
|
<span class="field__hint">
|
||||||
Standard: Grundfunktionen. Individuell: Module frei waehlbar. Enterprise: alle
|
Standard: Grundfunktionen. Individüll: Module frei wählbar. Enterprise: alle
|
||||||
Module. Testlizenz: 30 Tage.
|
Module. Testlizenz: 30 Tage.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<fieldset class="fieldset" id="module-box">
|
<fieldset class="fieldset" id="module-box">
|
||||||
<legend>Module (nur bei „Individuell“)</legend>
|
<legend>Module (nur bei „Individüll“)</legend>
|
||||||
<div class="stack stack--tight">
|
<div class="stack stack--tight">
|
||||||
{% for module in modules %}
|
{% for module in modules %}
|
||||||
{{ checkbox('modules', module_labels[module], value=module) }}
|
{{ checkbox('modules', module_labels[module], value=module) }}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
{{ field('valid_days', 'Laufzeit (Tage)', value=default_valid_days, type='number') }}
|
{{ field('valid_days', 'Laufzeit (Tage)', value=default_valid_days, type='number') }}
|
||||||
</div>
|
</div>
|
||||||
{{ checkbox('lifetime', 'Unbefristet',
|
{{ checkbox('lifetime', 'Unbefristet',
|
||||||
hint_text='Nicht kombinierbar mit einer Testlizenz -- das waere eine
|
hint_text='Nicht kombinierbar mit einer Testlizenz -- das wäre eine
|
||||||
permanente Lizenz ohne Module.') }}
|
permanente Lizenz ohne Module.') }}
|
||||||
{{ textarea_field('notes', 'Interne Notiz', rows=2) }}
|
{{ textarea_field('notes', 'Interne Notiz', rows=2) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -77,15 +77,15 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('share', size=17) }} Lizenzserver-Lizenz</div>
|
<div class="card__title">{{ icon('share', size=17) }} Lizenzserver-Lizenz</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Erzeugt eine neue Vertrauensgrenze -- der Empfaenger verwaltet danach eigene Kunden
|
Erzeugt eine neue Vertrauensgrenze -- der Empfänger verwaltet danach eigene Kunden
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{{ alert('Ein so lizenzierter Lizenzserver darf alles, was ein Lizenzserver koennen
|
{{ alert('Ein so lizenzierter Lizenzserver darf alles, was ein Lizenzserver können
|
||||||
soll -- ausser selbst weitere Lizenzserver-Lizenzen auszustellen. Er meldet seinen
|
soll -- ausser selbst weitere Lizenzserver-Lizenzen auszustellen. Er meldet seinen
|
||||||
Bestand automatisch hierher zurueck. Module sind fuer diese Rolle bedeutungslos und
|
Bestand automatisch hierher zurück. Module sind für diese Rolle bedeutungslos und
|
||||||
werden ignoriert. Self-Service und Kundenportal sind fuer solche Tickets gesperrt.',
|
werden ignoriert. Self-Service und Kundenportal sind für solche Tickets gesperrt.',
|
||||||
'warning') }}
|
'warning') }}
|
||||||
<label class="check">
|
<label class="check">
|
||||||
<input type="checkbox" name="role" value="license_server">
|
<input type="checkbox" name="role" value="license_server">
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if endpoint_missing %}
|
{% if endpoint_missing %}
|
||||||
{{ alert(endpoint_missing, 'danger', 'Keine Ausstellung moeglich') }}
|
{{ alert(endpoint_missing, 'danger', 'Keine Ausstellung möglich') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ alert('Ein Ticket beschreibt den Anspruch (Typ, Module, Laufzeit); die Lizenz ist die daraus
|
{{ alert('Ein Ticket beschreibt den Anspruch (Typ, Module, Laufzeit); die Lizenz ist die daraus
|
||||||
erzeugte, signierte Datei. Aenderungen am Ticket wirken auf die naechste
|
erzeugte, signierte Datei. Änderungen am Ticket wirken auf die nächste
|
||||||
Ausstellung -- bereits ausgestellte Lizenzen bleiben unveraendert.', 'info') }}
|
Ausstellung -- bereits ausgestellte Lizenzen bleiben unverändert.', 'info') }}
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -85,7 +85,7 @@ Ausstellung -- bereits ausgestellte Lizenzen bleiben unveraendert.', 'info') }}
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if not tickets %}
|
{% if not tickets %}
|
||||||
{{ empty_state('Keine Tickets', 'Legen Sie das erste Ticket fuer einen Kunden an.', 'ticket') }}
|
{{ empty_state('Keine Tickets', 'Legen Sie das erste Ticket für einen Kunden an.', 'ticket') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -59,7 +59,20 @@ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
|||||||
LockPersonality=yes
|
LockPersonality=yes
|
||||||
MemoryDenyWriteExecute=no
|
MemoryDenyWriteExecute=no
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
SystemCallFilter=@system-service
|
# "@mount" zusaetzlich zu @system-service: die Dateifreigaben werden ueber
|
||||||
|
# mount.cifs eingebunden, und @system-service enthaelt mount/umount2 nicht.
|
||||||
|
# Ohne diese Ergaenzung scheitert jede Einbindung mit "mount error(1):
|
||||||
|
# Operation not permitted" -- gemessen, und zwar unabhaengig von Rechten,
|
||||||
|
# Zugangsdaten und Mount-Optionen: aus einer normalen Root-Shell gelingt
|
||||||
|
# derselbe Aufruf sofort. Der Umweg ueber nsenter in den Namensraum des Wirts
|
||||||
|
# ist keine Alternative, weil "RestrictNamespaces=yes" setns verbietet.
|
||||||
|
#
|
||||||
|
# Eingebunden wird damit im Mount-Namensraum *dieses* Dienstes. Das ist genau
|
||||||
|
# richtig: nur er liest die Dateien, andere Prozesse haben dort nichts zu
|
||||||
|
# suchen, und mit dem Dienst verschwinden die Einbindungen wieder. Die
|
||||||
|
# Aufraeumaufgabe fuer abgelaufene Einbindungen laeuft deshalb im Webprozess --
|
||||||
|
# der Ueberwachungsdienst koennte sie nicht einmal sehen.
|
||||||
|
SystemCallFilter=@system-service @mount
|
||||||
SystemCallErrorNumber=EPERM
|
SystemCallErrorNumber=EPERM
|
||||||
# Bewusst **kein** einschraenkendes CapabilityBoundingSet: sudo braucht
|
# Bewusst **kein** einschraenkendes CapabilityBoundingSet: sudo braucht
|
||||||
# CAP_SETUID/CAP_SETGID, und der Helfer dahinter braucht Root. Was hier fehlt,
|
# CAP_SETUID/CAP_SETGID, und der Helfer dahinter braucht Root. Was hier fehlt,
|
||||||
|
|||||||
+65
-65
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Dieses Dokument richtet sich an die Person, die die Anlage installiert,
|
Dieses Dokument richtet sich an die Person, die die Anlage installiert,
|
||||||
aktualisiert und im Fehlerfall wieder zum Laufen bringt. Es beschreibt, was auf
|
aktualisiert und im Fehlerfall wieder zum Laufen bringt. Es beschreibt, was auf
|
||||||
dem Host tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird.
|
dem Host tatsächlich passiert -- nicht, wie die Oberfläche bedient wird.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ dem Host tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird.
|
|||||||
| Zweck | Pfad |
|
| Zweck | Pfad |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Anwendung (Code, venv, statische Dateien) | `/srv/<name>/` |
|
| Anwendung (Code, venv, statische Dateien) | `/srv/<name>/` |
|
||||||
| Instanzdaten (Datenbank, Schluessel, Lizenz) | `/srv/<name>/data/` |
|
| Instanzdaten (Datenbank, Schlüssel, Lizenz) | `/srv/<name>/data/` |
|
||||||
| Protokolle | `/var/log/<name>/` |
|
| Protokolle | `/var/log/<name>/` |
|
||||||
| Umgebungsdatei | `/etc/tesm/<name>.env` |
|
| Umgebungsdatei | `/etc/tesm/<name>.env` |
|
||||||
| Zertifikate (selbstsigniert) | `/etc/tesm/certs/<name>/` |
|
| Zertifikate (selbstsigniert) | `/etc/tesm/certs/<name>/` |
|
||||||
@@ -20,13 +20,13 @@ dem Host tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird.
|
|||||||
| Privilegierter Helfer | `/usr/local/lib/tesm/tesm-helper` |
|
| Privilegierter Helfer | `/usr/local/lib/tesm/tesm-helper` |
|
||||||
| Verwaltungswerkzeug | `/usr/local/bin/<name>-admin` |
|
| Verwaltungswerkzeug | `/usr/local/bin/<name>-admin` |
|
||||||
|
|
||||||
`<name>` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusaetzlichen
|
`<name>` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusätzlichen
|
||||||
Instanz, `tesm-<instanz>`. Dieser Name (der **SITE_KEY**) benennt alles, was
|
Instanz, `tesm-<instanz>`. Dieser Name (der **SITE_KEY**) benennt alles, was
|
||||||
ausserhalb von `/srv/<name>` liegt. Deshalb koennen zwei Installationen
|
ausserhalb von `/srv/<name>` liegt. Deshalb können zwei Installationen
|
||||||
derselben Anwendung nebeneinander laufen, ohne sich gegenseitig die
|
derselben Anwendung nebeneinander laufen, ohne sich gegenseitig die
|
||||||
nginx-Site, die netplan-Datei oder das Sitzungscookie zu ueberschreiben.
|
nginx-Site, die netplan-Datei oder das Sitzungscookie zu überschreiben.
|
||||||
|
|
||||||
Alles, was ein Mensch verlieren wuerde, liegt unter `data/`. Das Verzeichnis
|
Alles, was ein Mensch verlieren würde, liegt unter `data/`. Das Verzeichnis
|
||||||
wird von `install.sh` nie angefasst.
|
wird von `install.sh` nie angefasst.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -43,10 +43,10 @@ Wichtige Optionen:
|
|||||||
| Option | Wirkung |
|
| Option | Wirkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `--https` | Port 443 mit TLS, Port 80 leitet dorthin um. Ohne `--domain` entsteht ein selbstsigniertes Zertifikat. |
|
| `--https` | Port 443 mit TLS, Port 80 leitet dorthin um. Ohne `--domain` entsteht ein selbstsigniertes Zertifikat. |
|
||||||
| `--domain <name>` | Oeffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. |
|
| `--domain <name>` | Öffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. |
|
||||||
| `--acme-email <a>` | Erst zusammen mit `--domain` wird ein Let's-Encrypt-Zertifikat angefordert (und HSTS gesetzt). Ohne die Adresse bleibt es selbstsigniert -- interne Namen wie `lizenz.firma.local` kann keine CA bestaetigen. |
|
| `--acme-email <a>` | Erst zusammen mit `--domain` wird ein Let's-Encrypt-Zertifikat angefordert (und HSTS gesetzt). Ohne die Adresse bleibt es selbstsigniert -- interne Namen wie `lizenz.firma.local` kann keine CA bestätigen. |
|
||||||
| `--instance <name>` | Zusaetzliche Instanz neben der Hauptinstallation. |
|
| `--instance <name>` | Zusätzliche Instanz neben der Hauptinstallation. |
|
||||||
| `--port` / `--http-port` / `--https-port` | Abweichende Ports (nur fuer Testinstanzen sinnvoll). |
|
| `--port` / `--http-port` / `--https-port` | Abweichende Ports (nur für Testinstanzen sinnvoll). |
|
||||||
| `--skip-packages` | Keine Systempakete installieren. |
|
| `--skip-packages` | Keine Systempakete installieren. |
|
||||||
|
|
||||||
Danach:
|
Danach:
|
||||||
@@ -55,17 +55,17 @@ Danach:
|
|||||||
sudo -u tesm tesm-admin create-admin
|
sudo -u tesm tesm-admin create-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
Beim Lizenzserver zusaetzlich, **in dieser Reihenfolge**:
|
Beim Lizenzserver zusätzlich, **in dieser Reihenfolge**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u tesm-license tesm-license-admin init-key # Signaturschluessel -- sofort sichern!
|
sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel -- sofort sichern!
|
||||||
sudo -u tesm-license tesm-license-admin bootstrap # eigene Master-Lizenz
|
sudo -u tesm-license tesm-license-admin bootstrap # eigene Master-Lizenz
|
||||||
sudo -u tesm-license tesm-license-admin create-admin
|
sudo -u tesm-license tesm-license-admin create-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Der Signaturschluessel ist nicht wiederherstellbar.** Geht
|
> **Der Signaturschlüssel ist nicht wiederherstellbar.** Geht
|
||||||
> `data/master_signing_key.json` verloren, ist keine ausgestellte Lizenz mehr
|
> `data/master_signing_key.json` verloren, ist keine ausgestellte Lizenz mehr
|
||||||
> pruefbar. Sichern Sie ihn getrennt vom Server.
|
> prüfbar. Sichern Sie ihn getrennt vom Server.
|
||||||
|
|
||||||
### Zwei Anwendungen auf einem Host
|
### Zwei Anwendungen auf einem Host
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ sudo bash deploy/install.sh --app tesm-license --https --domain lizenz.firma.loc
|
|||||||
|
|
||||||
Die Installation **ohne** Domain wird zur Vorgabe-Site (`default_server`) und
|
Die Installation **ohne** Domain wird zur Vorgabe-Site (`default_server`) und
|
||||||
beantwortet alles, was zu keinem Namen passt. Zwei namenlose Sites auf
|
beantwortet alles, was zu keinem Namen passt. Zwei namenlose Sites auf
|
||||||
demselben Port weist nginx ab -- richtig so, und beim Uebernehmen sofort
|
demselben Port weist nginx ab -- richtig so, und beim Übernehmen sofort
|
||||||
sichtbar.
|
sichtbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -90,14 +90,14 @@ sudo bash deploy/install.sh --app tesm
|
|||||||
```
|
```
|
||||||
|
|
||||||
Derselbe Befehl. Das Skript erkennt an der vorhandenen Datenbank, dass es eine
|
Derselbe Befehl. Das Skript erkennt an der vorhandenen Datenbank, dass es eine
|
||||||
Aktualisierung ist, und tut dann zusaetzlich:
|
Aktualisierung ist, und tut dann zusätzlich:
|
||||||
|
|
||||||
1. **Vollstaendige Sicherung** nach `/srv/<name>-backup-<zeitstempel>` --
|
1. **Vollständige Sicherung** nach `/srv/<name>-backup-<zeitstempel>` --
|
||||||
unabhaengig davon, ob sich das Schema geaendert hat.
|
unabhängig davon, ob sich das Schema geändert hat.
|
||||||
2. Dateiabgleich; `data/` bleibt unangetastet.
|
2. Dateiabgleich; `data/` bleibt unangetastet.
|
||||||
3. Schema-Migrationen beim Start (versioniert, mit Pruefsumme).
|
3. Schema-Migrationen beim Start (versioniert, mit Prüfsumme).
|
||||||
4. **Gesundheitspruefung.** Antwortet die Anwendung nicht, wird der alte Stand
|
4. **Gesundheitsprüfung.** Antwortet die Anwendung nicht, wird der alte Stand
|
||||||
automatisch zurueckgeholt; die fehlgeschlagene Version bleibt unter
|
automatisch zurückgeholt; die fehlgeschlagene Version bleibt unter
|
||||||
`/srv/<name>-failed-<zeitstempel>` zur Analyse liegen.
|
`/srv/<name>-failed-<zeitstempel>` zur Analyse liegen.
|
||||||
|
|
||||||
Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
|
Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
|
||||||
@@ -114,7 +114,7 @@ Drei Wege dorthin:
|
|||||||
|
|
||||||
**a) Bei der Installation** -- siehe oben, `--https`.
|
**a) Bei der Installation** -- siehe oben, `--https`.
|
||||||
|
|
||||||
**b) Nachtraeglich ueber die Kommandozeile:**
|
**b) Nachträglich über die Kommandozeile:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u tesm tesm-admin web-setup --https --self-signed --apply
|
sudo -u tesm tesm-admin web-setup --https --self-signed --apply
|
||||||
@@ -124,42 +124,42 @@ sudo -u tesm tesm-admin web-setup --https --domain tesm.firma.de \
|
|||||||
|
|
||||||
`--print-config` zeigt die Datei, ohne etwas zu schreiben.
|
`--print-config` zeigt die Datei, ohne etwas zu schreiben.
|
||||||
|
|
||||||
**c) In der Oberflaeche** unter *Einstellungen -> Webserver und TLS*.
|
**c) In der Oberfläche** unter *Einstellungen -> Webserver und TLS*.
|
||||||
|
|
||||||
Danach muss `COOKIE_SECURE` in `/etc/tesm/<name>.env` auf `1` stehen (alle drei
|
Danach muss `COOKIE_SECURE` in `/etc/tesm/<name>.env` auf `1` stehen (alle drei
|
||||||
Wege erledigen das selbst) und der Dienst neu gestartet werden.
|
Wege erledigen das selbst) und der Dienst neu gestartet werden.
|
||||||
|
|
||||||
### Die Site-Datei wird immer neu erzeugt
|
### Die Site-Datei wird immer neu erzeugt
|
||||||
|
|
||||||
Sie ist **kein** Ort fuer Handarbeit. Jede Uebernahme schreibt sie vollstaendig
|
Sie ist **kein** Ort für Handarbeit. Jede Übernahme schreibt sie vollständig
|
||||||
aus der Vorlage neu. Was frueher von Hand hineingeschrieben wurde -- Domain,
|
aus der Vorlage neu. Was früher von Hand hineingeschrieben wurde -- Domain,
|
||||||
Ports, Zertifikatspfade, HSTS, Upload-Grenze -- ist heute Einstellung in der
|
Ports, Zertifikatspfade, HSTS, Upload-Grenze -- ist heute Einstellung in der
|
||||||
Anwendung.
|
Anwendung.
|
||||||
|
|
||||||
Der Grund steht im Vorgaengerprojekt: dort wurde die Datei nur angelegt, wenn
|
Der Grund steht im Vorgängerprojekt: dort wurde die Datei nur angelegt, wenn
|
||||||
sie fehlte. Ein einmal falscher `alias`-Pfad ueberlebte dadurch jahrelang jedes
|
sie fehlte. Ein einmal falscher `alias`-Pfad überlebte dadurch jahrelang jedes
|
||||||
Update, und die Oberflaeche kam die ganze Zeit ohne Design.
|
Update, und die Oberfläche kam die ganze Zeit ohne Design.
|
||||||
|
|
||||||
### Selbstsigniert oder Let's Encrypt?
|
### Selbstsigniert oder Let's Encrypt?
|
||||||
|
|
||||||
* **Oeffentlich erreichbar, echter DNS-Name:** Let's Encrypt. Nur dann ist HSTS
|
* **Öffentlich erreichbar, echter DNS-Name:** Let's Encrypt. Nur dann ist HSTS
|
||||||
sinnvoll.
|
sinnvoll.
|
||||||
* **Geschlossenes Netz:** selbstsigniert. Browser zeigen eine Warnung, die
|
* **Geschlossenes Netz:** selbstsigniert. Browser zeigen eine Warnung, die
|
||||||
Verbindung ist trotzdem verschluesselt und das Sitzungscookie darf `Secure`
|
Verbindung ist trotzdem verschlüsselt und das Sitzungscookie darf `Secure`
|
||||||
tragen. **HSTS hier nicht einschalten** -- der Browser merkt sich die Vorgabe
|
tragen. **HSTS hier nicht einschalten** -- der Browser merkt sich die Vorgabe
|
||||||
monatelang und die Warnung laesst sich dann nicht mehr wegklicken.
|
monatelang und die Warnung lässt sich dann nicht mehr wegklicken.
|
||||||
|
|
||||||
Die Erneuerung von Let's Encrypt laeuft ueber `certbot certonly --webroot`
|
Die Erneuerung von Let's Encrypt läuft über `certbot certonly --webroot`
|
||||||
(bewusst nicht ueber das nginx-Plugin, das die erzeugte Site umschreiben
|
(bewusst nicht über das nginx-Plugin, das die erzeugte Site umschreiben
|
||||||
wuerde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar.
|
würde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Dienste
|
## 5. Dienste
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl status tesm.service # Weboberflaeche (gunicorn)
|
systemctl status tesm.service # Weboberfläche (gunicorn)
|
||||||
systemctl status tesm-monitor.service # Ueberwachungsschleife (nur TESM)
|
systemctl status tesm-monitor.service # Überwachungsschleife (nur TESM)
|
||||||
systemctl status tesm-license.service
|
systemctl status tesm-license.service
|
||||||
journalctl -u tesm -n 100 -f
|
journalctl -u tesm -n 100 -f
|
||||||
```
|
```
|
||||||
@@ -179,8 +179,8 @@ Die Anwendung hat keine. Alles, was Root braucht, geht durch **einen** Pfad:
|
|||||||
/usr/local/lib/tesm/tesm-helper <verb> [argumente]
|
/usr/local/lib/tesm/tesm-helper <verb> [argumente]
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Helfer gehoert `root`, ist per sudoers ausschliesslich fuer genau diesen
|
Der Helfer gehört `root`, ist per sudoers ausschliesslich für genau diesen
|
||||||
Pfad mit `NOPASSWD` freigegeben und prueft jedes Argument gegen eine
|
Pfad mit `NOPASSWD` freigegeben und prüft jedes Argument gegen eine
|
||||||
Positivliste. Er reicht nie etwas an eine Shell weiter (kein `eval`, kein
|
Positivliste. Er reicht nie etwas an eine Shell weiter (kein `eval`, kein
|
||||||
`sh -c`).
|
`sh -c`).
|
||||||
|
|
||||||
@@ -191,16 +191,16 @@ sudo /usr/local/lib/tesm/tesm-helper
|
|||||||
grep -n ')$' /usr/local/lib/tesm/tesm-helper
|
grep -n ')$' /usr/local/lib/tesm/tesm-helper
|
||||||
```
|
```
|
||||||
|
|
||||||
Regel fuer Aenderungen: **Jedes neue Verb braucht eine Argumentpruefung.**
|
Regel für Änderungen: **Jedes neue Verb braucht eine Argumentprüfung.**
|
||||||
`tests/test_repo_hygiene.py` erzwingt, dass jedes der Anwendung bekannte Verb
|
`tests/test_repo_hygiene.py` erzwingt, dass jedes der Anwendung bekannte Verb
|
||||||
im Helfer vorkommt und dass jede Pfadpruefung `..` abweist.
|
im Helfer vorkommt und dass jede Pfadprüfung `..` abweist.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Sicherung und Wiederherstellung
|
## 7. Sicherung und Wiederherstellung
|
||||||
|
|
||||||
In der Oberflaeche unter *Sicherung* -- verschluesselte Ausfuhr (Argon2id +
|
In der Oberfläche unter *Sicherung* -- verschlüsselte Ausfuhr (Argon2id +
|
||||||
AES-256-GCM) mit einem selbst gewaehlten Kennwort. Der Import zeigt zuerst eine
|
AES-256-GCM) mit einem selbst gewählten Kennwort. Der Import zeigt zuerst eine
|
||||||
Vorschau, bevor irgendetwas geschrieben wird.
|
Vorschau, bevor irgendetwas geschrieben wird.
|
||||||
|
|
||||||
Auf dem Host reicht:
|
Auf dem Host reicht:
|
||||||
@@ -214,10 +214,10 @@ sudo systemctl start tesm
|
|||||||
Unverzichtbar:
|
Unverzichtbar:
|
||||||
|
|
||||||
* `data/app.db` -- alle Daten
|
* `data/app.db` -- alle Daten
|
||||||
* `data/secret.key` -- ohne sie sind alle Sitzungen ungueltig
|
* `data/secret.key` -- ohne sie sind alle Sitzungen ungültig
|
||||||
* `data/data.keys` -- ohne sie sind **alle gespeicherten Geheimnisse verloren**
|
* `data/data.keys` -- ohne sie sind **alle gespeicherten Geheimnisse verloren**
|
||||||
* `data/license.json` und `data/license_key.json`
|
* `data/license.json` und `data/license_key.json`
|
||||||
* Lizenzserver zusaetzlich: `data/master_signing_key.json`
|
* Lizenzserver zusätzlich: `data/master_signing_key.json`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -230,17 +230,17 @@ curl -s http://127.0.0.1:5000/gesundheit
|
|||||||
```
|
```
|
||||||
Antwortet gunicorn, aber der Browser nicht, liegt es an nginx.
|
Antwortet gunicorn, aber der Browser nicht, liegt es an nginx.
|
||||||
|
|
||||||
**nginx laesst sich nicht neu laden.**
|
**nginx lässt sich nicht neu laden.**
|
||||||
```bash
|
```bash
|
||||||
sudo nginx -t
|
sudo nginx -t
|
||||||
```
|
```
|
||||||
Die Uebernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst
|
Die Übernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst
|
||||||
zurueck. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft
|
zurück. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft
|
||||||
`install.sh`, das eine unbrauchbare Site durch die Vorlage ersetzt.
|
`install.sh`, das eine unbrauchbare Site durch die Vorlage ersetzt.
|
||||||
|
|
||||||
**Anmeldung schlaegt ohne Fehlermeldung fehl.**
|
**Anmeldung schlägt ohne Fehlermeldung fehl.**
|
||||||
Fast immer `COOKIE_SECURE=1` ohne HTTPS. Der Browser bekommt dann ein Cookie,
|
Fast immer `COOKIE_SECURE=1` ohne HTTPS. Der Browser bekommt dann ein Cookie,
|
||||||
das er ueber HTTP nie zuruecksendet.
|
das er über HTTP nie zurücksendet.
|
||||||
```bash
|
```bash
|
||||||
grep COOKIE_SECURE /etc/tesm/tesm.env
|
grep COOKIE_SECURE /etc/tesm/tesm.env
|
||||||
```
|
```
|
||||||
@@ -250,38 +250,38 @@ Der Reverse Proxy reicht den Host falsch durch. In der Site-Datei muss
|
|||||||
`proxy_set_header Host $http_host;` stehen -- **nicht** `$host`, das verwirft
|
`proxy_set_header Host $http_host;` stehen -- **nicht** `$host`, das verwirft
|
||||||
den Port.
|
den Port.
|
||||||
|
|
||||||
**Statische Dateien fehlen, die Oberflaeche ist unformatiert.**
|
**Statische Dateien fehlen, die Oberfläche ist unformatiert.**
|
||||||
```bash
|
```bash
|
||||||
grep alias /etc/nginx/sites-available/tesm
|
grep alias /etc/nginx/sites-available/tesm
|
||||||
ls /srv/tesm/static /srv/tesm/static-core
|
ls /srv/tesm/static /srv/tesm/static-core
|
||||||
```
|
```
|
||||||
Beide `alias`-Pfade muessen in das Installationsverzeichnis zeigen.
|
Beide `alias`-Pfade müssen in das Installationsverzeichnis zeigen.
|
||||||
|
|
||||||
**Zwei Instanzen stoeren sich.**
|
**Zwei Instanzen stören sich.**
|
||||||
`SITE_KEY` in beiden `/etc/tesm/*.env` vergleichen -- er muss sich
|
`SITE_KEY` in beiden `/etc/tesm/*.env` vergleichen -- er muss sich
|
||||||
unterscheiden. Sonst teilen sich beide nginx-Site und Sitzungscookie.
|
unterscheiden. Sonst teilen sich beide nginx-Site und Sitzungscookie.
|
||||||
|
|
||||||
**Die Wartung meldet "Der SSH-Host-Schluessel ist unbekannt".**
|
**Die Wartung meldet "Der SSH-Host-Schlüssel ist unbekannt".**
|
||||||
Kein Fehler, sondern die Absicht: ein Hintergrundauftrag darf keinen fremden
|
Kein Fehler, sondern die Absicht: ein Hintergrundauftrag darf keinen fremden
|
||||||
Schluessel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf
|
Schlüssel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf
|
||||||
der Detailseite des Geraets (*Clients -> Geraet -> SSH-Host-Schluessel*):
|
der Detailseite des Geräts (*Clients -> Gerät -> SSH-Host-Schlüssel*):
|
||||||
erst **Auslesen**, dann den Fingerprint mit einer unabhaengigen Quelle
|
erst **Auslesen**, dann den Fingerprint mit einer unabhängigen Quelle
|
||||||
vergleichen, dann **Freigeben**. Die Wartungsuebersicht zeigt in einer eigenen
|
vergleichen, dann **Freigeben**. Die Wartungsübersicht zeigt in einer eigenen
|
||||||
Spalte, fuer welche Geraete das noch offen ist. Fuer Switche liegt dieselbe
|
Spalte, für welche Geräte das noch offen ist. Für Switche liegt dieselbe
|
||||||
Freigabe auf der Switch-Detailseite.
|
Freigabe auf der Switch-Detailseite.
|
||||||
|
|
||||||
Der Schluessel wird nach **IP und Port** gespeichert, nicht nach Name -- TESM
|
Der Schlüssel wird nach **IP und Port** gespeichert, nicht nach Name -- TESM
|
||||||
verbindet sich immer ueber die hinterlegte IP-Adresse. Aendert sich die IP
|
verbindet sich immer über die hinterlegte IP-Adresse. Ändert sich die IP
|
||||||
eines Geraets, ist der Schluessel erneut freizugeben.
|
eines Geräts, ist der Schlüssel erneut freizugeben.
|
||||||
|
|
||||||
**Die Pruefkette des Aenderungsprotokolls ist gebrochen.**
|
**Die Prüfkette des Änderungsprotokolls ist gebrochen.**
|
||||||
Unter *Diagnose* sichtbar. Das Protokoll ist verkettet gehasht; ein Bruch heisst,
|
Unter *Diagnose* sichtbar. Das Protokoll ist verkettet gehasht; ein Bruch heisst,
|
||||||
dass jemand direkt in die Datenbank geschrieben hat. Der Eintrag, ab dem es
|
dass jemand direkt in die Datenbank geschrieben hat. Der Eintrag, ab dem es
|
||||||
nicht mehr stimmt, wird genannt.
|
nicht mehr stimmt, wird genannt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Zurueck auf eine aeltere Version
|
## 9. Zurück auf eine ältere Version
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl stop tesm
|
sudo systemctl stop tesm
|
||||||
@@ -290,6 +290,6 @@ sudo cp -a /srv/tesm-backup-<zeitstempel> /srv/tesm
|
|||||||
sudo systemctl start tesm
|
sudo systemctl start tesm
|
||||||
```
|
```
|
||||||
|
|
||||||
Schema-Migrationen laufen nur vorwaerts. Ein Ruecksprung ueber eine
|
Schema-Migrationen laufen nur vorwärts. Ein Rücksprung über eine
|
||||||
Schema-Aenderung hinweg braucht deshalb auch die Datenbank aus der Sicherung --
|
Schema-Änderung hinweg braucht deshalb auch die Datenbank aus der Sicherung --
|
||||||
genau das liegt im Backup-Verzeichnis.
|
genau das liegt im Backup-Verzeichnis.
|
||||||
|
|||||||
+56
-56
@@ -1,6 +1,6 @@
|
|||||||
# Sicherheit
|
# Sicherheit
|
||||||
|
|
||||||
Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind.
|
Was geschützt wird, womit, und wo bewusst Grenzen gezogen sind.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,35 +9,35 @@ Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind.
|
|||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Speicherung | Argon2id (argon2-cffi) |
|
| Speicherung | Argon2id (argon2-cffi) |
|
||||||
| Altbestand | bcrypt wird noch geprueft und bei der naechsten erfolgreichen Anmeldung stillschweigend auf Argon2id umgestellt |
|
| Altbestand | bcrypt wird noch geprüft und bei der nächsten erfolgreichen Anmeldung stillschweigend auf Argon2id umgestellt |
|
||||||
| Zweiter Faktor | TOTP, optional erzwingbar fuer Administratoren |
|
| Zweiter Faktor | TOTP, optional erzwingbar für Administratoren |
|
||||||
| Sperre je Konto | nach `LOGIN_MAX_ATTEMPTS` Versuchen |
|
| Sperre je Konto | nach `LOGIN_MAX_ATTEMPTS` Versuchen |
|
||||||
| Sperre je Adresse | zusaetzlich `LOGIN_MAX_ATTEMPTS_PER_IP` -- sonst laesst sich die Kontosperre umgehen, indem man viele Konten je einmal probiert |
|
| Sperre je Adresse | zusätzlich `LOGIN_MAX_ATTEMPTS_PER_IP` -- sonst lässt sich die Kontosperre umgehen, indem man viele Konten je einmal probiert |
|
||||||
| Verzeichnisdienst | LDAP nur ueber TLS, Zertifikat wird geprueft. Ohne gueltiges Zertifikat keine Anmeldung -- kein Schalter, der das abschaltet |
|
| Verzeichnisdienst | LDAP nur über TLS, Zertifikat wird geprüft. Ohne gültiges Zertifikat keine Anmeldung -- kein Schalter, der das abschaltet |
|
||||||
|
|
||||||
Nach einer Anmeldung wird die Sitzungskennung neu vergeben (gegen Session
|
Nach einer Anmeldung wird die Sitzungskennung neu vergeben (gegen Session
|
||||||
Fixation) und das CSRF-Token gewechselt.
|
Fixation) und das CSRF-Token gewechselt.
|
||||||
|
|
||||||
### Erneute Bestaetigung ("sudo-Modus")
|
### Erneute Bestätigung ("sudo-Modus")
|
||||||
|
|
||||||
Alles, was Geheimnisse offenlegt oder Daten aus dem System heraustraegt,
|
Alles, was Geheimnisse offenlegt oder Daten aus dem System herausträgt,
|
||||||
verlangt eine frische Passworteingabe: Klartextanzeige von Zugangsdaten,
|
verlangt eine frische Passworteingabe: Klartextanzeige von Zugangsdaten,
|
||||||
Ausfuhr, Herunterladen eines Lizenzbundles, Schluesselrotation,
|
Ausfuhr, Herunterladen eines Lizenzbundles, Schlüsselrotation,
|
||||||
2FA-Aenderungen, Deaktivierung einer Lizenz.
|
2FA-Änderungen, Deaktivierung einer Lizenz.
|
||||||
|
|
||||||
Ein uebernommenes Sitzungscookie allein reicht dafuer nicht.
|
Ein übernommenes Sitzungscookie allein reicht dafür nicht.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Sitzungen
|
## 2. Sitzungen
|
||||||
|
|
||||||
Serverseitig in SQLite, nicht im Cookie. Das Cookie traegt nur eine
|
Serverseitig in SQLite, nicht im Cookie. Das Cookie trägt nur eine
|
||||||
Zufallskennung.
|
Zufallskennung.
|
||||||
|
|
||||||
* `HttpOnly`, `SameSite=Lax`, `Secure` sobald HTTPS eingerichtet ist
|
* `HttpOnly`, `SameSite=Lax`, `Secure` sobald HTTPS eingerichtet ist
|
||||||
* Leerlaufzeit **und** absolute Hoechstdauer
|
* Leerlaufzeit **und** absolute Höchstdauer
|
||||||
* Der Cookiename enthaelt den `SITE_KEY` -- zwei Installationen auf einem Host
|
* Der Cookiename enthält den `SITE_KEY` -- zwei Installationen auf einem Host
|
||||||
koennen sich die Sitzung nicht gegenseitig ueberschreiben (Cookies
|
können sich die Sitzung nicht gegenseitig überschreiben (Cookies
|
||||||
unterscheiden keine Ports)
|
unterscheiden keine Ports)
|
||||||
* Aktive Sitzungen sind einsehbar und einzeln beendbar
|
* Aktive Sitzungen sind einsehbar und einzeln beendbar
|
||||||
|
|
||||||
@@ -45,22 +45,22 @@ Zufallskennung.
|
|||||||
|
|
||||||
## 3. CSRF
|
## 3. CSRF
|
||||||
|
|
||||||
Drei voneinander unabhaengige Schichten:
|
Drei voneinander unabhängige Schichten:
|
||||||
|
|
||||||
1. `SameSite=Lax` am Sitzungscookie.
|
1. `SameSite=Lax` am Sitzungscookie.
|
||||||
2. Ein Token je Sitzung, in jedem Formular, in konstanter Zeit verglichen.
|
2. Ein Token je Sitzung, in jedem Formular, in konstanter Zeit verglichen.
|
||||||
3. Origin-/Referer-Pruefung gegen den Host der Anfrage.
|
3. Origin-/Referer-Prüfung gegen den Host der Anfrage.
|
||||||
|
|
||||||
Die dritte Schicht akzeptiert den Host mit und ohne Port sowie die
|
Die dritte Schicht akzeptiert den Host mit und ohne Port sowie die
|
||||||
Standardports -- **nicht** aber beliebige Ports: ein anderer Dienst auf
|
Standardports -- **nicht** aber beliebige Ports: ein anderer Dienst auf
|
||||||
demselben Rechner ist ein anderer Ursprung.
|
demselben Rechner ist ein anderer Ursprung.
|
||||||
|
|
||||||
Der Vorgaenger hatte gar keinen CSRF-Schutz. Eine praeparierte Seite konnte im
|
Der Vorgänger hatte gar keinen CSRF-Schutz. Eine präparierte Seite konnte im
|
||||||
Namen eines angemeldeten Administrators Geraete loeschen oder Benutzer anlegen.
|
Namen eines angemeldeten Administrators Geräte löschen oder Benutzer anlegen.
|
||||||
|
|
||||||
Ausnahmen (`@csrf.exempt`) gibt es nur fuer Maschinenschnittstellen, die sich
|
Ausnahmen (`@csrf.exempt`) gibt es nur für Maschinenschnittstellen, die sich
|
||||||
**nicht** ueber Cookies autorisieren. Eine cookie-autorisierte Route ohne
|
**nicht** über Cookies autorisieren. Eine cookie-autorisierte Route ohne
|
||||||
CSRF-Schutz waere eine Luecke.
|
CSRF-Schutz wäre eine Lücke.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -69,75 +69,75 @@ CSRF-Schutz waere eine Luecke.
|
|||||||
Bei jeder Antwort:
|
Bei jeder Antwort:
|
||||||
|
|
||||||
* `Content-Security-Policy` mit einem Nonce je Anfrage. **Kein**
|
* `Content-Security-Policy` mit einem Nonce je Anfrage. **Kein**
|
||||||
`unsafe-inline` fuer Skripte -- deshalb gibt es im ganzen Projekt kein
|
`unsafe-inline` für Skripte -- deshalb gibt es im ganzen Projekt kein
|
||||||
Inline-Script ohne Nonce, und JavaScript haengt ueber
|
Inline-Script ohne Nonce, und JavaScript hängt über
|
||||||
`data-behavior`-Attribute am DOM. `style-src` erlaubt `unsafe-inline`, weil
|
`data-behavior`-Attribute am DOM. `style-src` erlaubt `unsafe-inline`, weil
|
||||||
einzelne berechnete Breiten (Fortschrittsbalken) als Style-Attribut gesetzt
|
einzelne berechnete Breiten (Fortschrittsbalken) als Style-Attribut gesetzt
|
||||||
werden; Style-Attribute fuehren keinen Code aus.
|
werden; Style-Attribute führen keinen Code aus.
|
||||||
* `X-Content-Type-Options: nosniff`
|
* `X-Content-Type-Options: nosniff`
|
||||||
* `X-Frame-Options: DENY`, `frame-ancestors 'none'`
|
* `X-Frame-Options: DENY`, `frame-ancestors 'none'`
|
||||||
* `Referrer-Policy: same-origin`
|
* `Referrer-Policy: same-origin`
|
||||||
* `Permissions-Policy` -- Kamera, Mikrofon, Ort, Zahlung, USB abgeschaltet
|
* `Permissions-Policy` -- Kamera, Mikrofon, Ort, Zahlung, USB abgeschaltet
|
||||||
* `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`
|
* `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`
|
||||||
* `Strict-Transport-Security` nur, wenn TLS tatsaechlich terminiert wird
|
* `Strict-Transport-Security` nur, wenn TLS tatsächlich terminiert wird
|
||||||
|
|
||||||
> HSTS gehoert **nicht** zu einem selbstsignierten Zertifikat. Der Browser
|
> HSTS gehört **nicht** zu einem selbstsignierten Zertifikat. Der Browser
|
||||||
> merkt sich die Vorgabe monatelang; die Warnung laesst sich danach nicht mehr
|
> merkt sich die Vorgabe monatelang; die Warnung lässt sich danach nicht mehr
|
||||||
> wegklicken. `install.sh` setzt HSTS deshalb nur zusammen mit Let's Encrypt.
|
> wegklicken. `install.sh` setzt HSTS deshalb nur zusammen mit Let's Encrypt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Geheimnisse im Ruhezustand
|
## 5. Geheimnisse im Ruhezustand
|
||||||
|
|
||||||
Ein Schluesselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600):
|
Ein Schlüsselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600):
|
||||||
|
|
||||||
* Versionierte Schluessel, rotierbar. Chiffrate tragen ihre Schluesselkennung
|
* Versionierte Schlüssel, rotierbar. Chiffrate tragen ihre Schlüsselkennung
|
||||||
(`v1.<id>.<nonce>.<ct>`) und bleiben nach einer Rotation lesbar.
|
(`v1.<id>.<nonce>.<ct>`) und bleiben nach einer Rotation lesbar.
|
||||||
* Der Verwendungszweck geht als AAD in die Verschluesselung ein. Ein Chiffrat
|
* Der Verwendungszweck geht als AAD in die Verschlüsselung ein. Ein Chiffrat
|
||||||
aus einem Kontext laesst sich in einem anderen nicht entschluesseln.
|
aus einem Kontext lässt sich in einem anderen nicht entschlüsseln.
|
||||||
* Beim Start wird geprueft, ob die Rechte auf Schluessel- und Lizenzdateien
|
* Beim Start wird geprüft, ob die Rechte auf Schlüssel- und Lizenzdateien
|
||||||
noch stimmen; Abweichungen erscheinen unter *Diagnose*.
|
noch stimmen; Abweichungen erscheinen unter *Diagnose*.
|
||||||
|
|
||||||
Passwoerter fuer Mounts gehen ueber `stdin` an den privilegierten Helfer, der
|
Passwörter für Mounts gehen über `stdin` an den privilegierten Helfer, der
|
||||||
sie in eine 0600-Datei schreibt -- nie als Kommandozeilenargument (Prozessliste)
|
sie in eine 0600-Datei schreibt -- nie als Kommandozeilenargument (Prozessliste)
|
||||||
und nie ueber die Umgebung (`/proc/<pid>/environ`).
|
und nie über die Umgebung (`/proc/<pid>/environ`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Rechtetrennung auf dem Host
|
## 6. Rechtetrennung auf dem Host
|
||||||
|
|
||||||
Der Webprozess laeuft als `tesm` bzw. `tesm-license`. Root-Aktionen gehen durch
|
Der Webprozess läuft als `tesm` bzw. `tesm-license`. Root-Aktionen gehen durch
|
||||||
genau einen allowlisted Helfer (`/usr/local/lib/tesm/tesm-helper`), der per
|
genau einen allowlisted Helfer (`/usr/local/lib/tesm/tesm-helper`), der per
|
||||||
sudoers ausschliesslich fuer diesen Pfad freigegeben ist.
|
sudoers ausschliesslich für diesen Pfad freigegeben ist.
|
||||||
|
|
||||||
Regeln, die im Repository durch Tests erzwungen werden:
|
Regeln, die im Repository durch Tests erzwungen werden:
|
||||||
|
|
||||||
* Jedes Verb, das die Anwendung kennt, existiert im Helfer.
|
* Jedes Verb, das die Anwendung kennt, existiert im Helfer.
|
||||||
* Kein `eval`, kein `bash -c`, kein `sh -c`.
|
* Kein `eval`, kein `bash -c`, kein `sh -c`.
|
||||||
* Jede Pfadpruefung weist `..` ab -- sonst laesst sich eine Positivliste ueber
|
* Jede Pfadprüfung weist `..` ab -- sonst lässt sich eine Positivliste über
|
||||||
den Basisnamen unterlaufen (`/etc/nginx/sites-available/../../../root/x` hat
|
den Basisnamen unterlaufen (`/etc/nginx/sites-available/../../../root/x` hat
|
||||||
den zulaessigen Basisnamen `x`).
|
den zulässigen Basisnamen `x`).
|
||||||
* Der Instanzname ist auf `tesm[-<instanz>]` begrenzt; er landet in Pfaden.
|
* Der Instanzname ist auf `tesm[-<instanz>]` begrenzt; er landet in Pfaden.
|
||||||
* Keine sudoers-Regel ausser der einen. `NOPASSWD: ALL` gibt es nicht.
|
* Keine sudoers-Regel ausser der einen. `NOPASSWD: ALL` gibt es nicht.
|
||||||
|
|
||||||
Die systemd-Units haben `NoNewPrivileges=yes`, `ProtectSystem=strict`,
|
Die systemd-Units haben `NoNewPrivileges=yes`, `ProtectSystem=strict`,
|
||||||
`PrivateTmp=yes` und eine enge `RestrictAddressFamilies`-Liste.
|
`PrivateTmp=yes` und eine enge `RestrictAddressFamilies`-Liste.
|
||||||
|
|
||||||
Der Vorgaenger liess die komplette Flask-Anwendung als root laufen. Eine
|
Der Vorgänger liess die komplette Flask-Anwendung als root laufen. Eine
|
||||||
einzige Luecke in irgendeinem Pfad haette den Host bedeutet.
|
einzige Lücke in irgendeinem Pfad hätte den Host bedeutet.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Aenderungsprotokoll
|
## 7. Änderungsprotokoll
|
||||||
|
|
||||||
Verkettet gehasht: jeder Eintrag enthaelt den Hash seines Vorgaengers. Ein
|
Verkettet gehasht: jeder Eintrag enthält den Hash seines Vorgängers. Ein
|
||||||
nachtraegliches Aendern oder Loeschen faellt auf und wird unter *Diagnose*
|
nachträgliches Ändern oder Löschen fällt auf und wird unter *Diagnose*
|
||||||
angezeigt, mitsamt dem Eintrag, ab dem die Kette bricht.
|
angezeigt, mitsamt dem Eintrag, ab dem die Kette bricht.
|
||||||
|
|
||||||
Protokolliert wird, wer was wann von welcher Adresse getan hat -- ausdruecklich
|
Protokolliert wird, wer was wann von welcher Adresse getan hat -- ausdrücklich
|
||||||
auch Lesezugriffe auf Geheimnisse (`credential.secret_revealed`).
|
auch Lesezugriffe auf Geheimnisse (`credential.secret_revealed`).
|
||||||
|
|
||||||
Aeltere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
|
Ältere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -145,20 +145,20 @@ Aeltere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
|
|||||||
|
|
||||||
Siehe [ARCHITEKTUR.md](ARCHITEKTUR.md), Abschnitt Lizenzierung. Kurz:
|
Siehe [ARCHITEKTUR.md](ARCHITEKTUR.md), Abschnitt Lizenzierung. Kurz:
|
||||||
getrennte Signaturkontexte, kanonisches JSON, Nonce gegen Wiedereinspielung,
|
getrennte Signaturkontexte, kanonisches JSON, Nonce gegen Wiedereinspielung,
|
||||||
Frischefenster, `seq` gegen Rueckstufung, Antwort an die Anfrage gebunden,
|
Frischefenster, `seq` gegen Rückstufung, Antwort an die Anfrage gebunden,
|
||||||
Aussteller-Pinning.
|
Aussteller-Pinning.
|
||||||
|
|
||||||
Das Auslieferungsbundle enthaelt den privaten Clientschluessel und ist damit
|
Das Auslieferungsbundle enthält den privaten Clientschlüssel und ist damit
|
||||||
selbst ein Geheimnis: eigenes Recht (`licenses.export`), frische
|
selbst ein Geheimnis: eigenes Recht (`licenses.export`), frische
|
||||||
Passwortbestaetigung, `Cache-Control: no-store`, Eintrag im Zustell- und im
|
Passwortbestätigung, `Cache-Control: no-store`, Eintrag im Zustell- und im
|
||||||
Aenderungsprotokoll.
|
Änderungsprotokoll.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Ein- und Ausfuhr
|
## 9. Ein- und Ausfuhr
|
||||||
|
|
||||||
Verschluesselte Umschlaege (Argon2id-Ableitung + AES-256-GCM) mit einem selbst
|
Verschlüsselte Umschläge (Argon2id-Ableitung + AES-256-GCM) mit einem selbst
|
||||||
gewaehlten Kennwort. Der Import zeigt eine Vorschau, bevor etwas geschrieben
|
gewählten Kennwort. Der Import zeigt eine Vorschau, bevor etwas geschrieben
|
||||||
wird; die Vorschau liegt in der Datenbank, nicht im Prozessspeicher -- deshalb
|
wird; die Vorschau liegt in der Datenbank, nicht im Prozessspeicher -- deshalb
|
||||||
braucht der Dienst kein `--workers 1` mehr.
|
braucht der Dienst kein `--workers 1` mehr.
|
||||||
|
|
||||||
@@ -166,11 +166,11 @@ braucht der Dienst kein `--workers 1` mehr.
|
|||||||
|
|
||||||
## 10. Bewusst nicht getan
|
## 10. Bewusst nicht getan
|
||||||
|
|
||||||
* **Kein Schalter, der die LDAP-Zertifikatspruefung abschaltet.** Wer ihn
|
* **Kein Schalter, der die LDAP-Zertifikatsprüfung abschaltet.** Wer ihn
|
||||||
einmal setzt, setzt ihn dauerhaft.
|
einmal setzt, setzt ihn dauerhaft.
|
||||||
* **Kein CSRF-Freibrief fuer cookie-autorisierte Routen.**
|
* **Kein CSRF-Freibrief für cookie-autorisierte Routen.**
|
||||||
* **Kein `unsafe-inline` fuer Skripte**, auch nicht "voruebergehend".
|
* **Kein `unsafe-inline` für Skripte**, auch nicht "vorübergehend".
|
||||||
* **Keine Rechteausweitung**, auch nicht fuer Administratoren untereinander.
|
* **Keine Rechteausweitung**, auch nicht für Administratoren untereinander.
|
||||||
* **Kein Hostname im Lizenz-Fingerabdruck** -- Umbenennen darf die Bindung
|
* **Kein Hostname im Lizenz-Fingerabdruck** -- Umbenennen darf die Bindung
|
||||||
nicht brechen.
|
nicht brechen.
|
||||||
* **Keine von Hand gepflegte nginx-Site.** Sie wird immer neu erzeugt.
|
* **Keine von Hand gepflegte nginx-Site.** Sie wird immer neu erzeugt.
|
||||||
@@ -179,5 +179,5 @@ braucht der Dienst kein `--workers 1` mehr.
|
|||||||
|
|
||||||
## 11. Wenn Sie eine Schwachstelle finden
|
## 11. Wenn Sie eine Schwachstelle finden
|
||||||
|
|
||||||
Bitte nicht oeffentlich melden. Wenden Sie sich an die im Anbieterprofil des
|
Bitte nicht öffentlich melden. Wenden Sie sich an die im Anbieterprofil des
|
||||||
Lizenzservers hinterlegte Adresse.
|
Lizenzservers hinterlegte Adresse.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Design-System.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.0.2"
|
||||||
|
|
||||||
from .app_factory import create_base_app
|
from .app_factory import create_base_app
|
||||||
from .config import CoreConfig, SecurityConfig, load_core_config
|
from .config import CoreConfig, SecurityConfig, load_core_config
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ from .views import hostsettings as hostsettings_views
|
|||||||
from .views import logs as logs_views
|
from .views import logs as logs_views
|
||||||
from .views import trash as trash_views
|
from .views import trash as trash_views
|
||||||
|
|
||||||
|
#: Rechteinhaber, in dieser Reihenfolge. Steht an einer Stelle, weil die
|
||||||
|
#: Fusszeile in zwei Vorlagen auftaucht (Anwendungsrahmen und Anmeldeseiten)
|
||||||
|
#: und zwei Texte fuer dieselbe Aussage auseinanderlaufen.
|
||||||
|
COPYRIGHT_NOTICE = "Tim Eertmoed · WiS IT-Solutions GmbH"
|
||||||
|
|
||||||
CORE_SETTINGS: tuple[SettingSpec, ...] = (
|
CORE_SETTINGS: tuple[SettingSpec, ...] = (
|
||||||
SettingSpec("nav_order", [], "json", "Reihenfolge der Navigation", section="intern"),
|
SettingSpec("nav_order", [], "json", "Reihenfolge der Navigation", section="intern"),
|
||||||
SettingSpec("nav_child_order", {}, "json", "Reihenfolge der Unterpunkte", section="intern"),
|
SettingSpec("nav_child_order", {}, "json", "Reihenfolge der Unterpunkte", section="intern"),
|
||||||
@@ -56,6 +61,18 @@ CORE_SETTINGS: tuple[SettingSpec, ...] = (
|
|||||||
minimum=1,
|
minimum=1,
|
||||||
maximum=3650,
|
maximum=3650,
|
||||||
),
|
),
|
||||||
|
SettingSpec(
|
||||||
|
"session_idle_minutes",
|
||||||
|
30,
|
||||||
|
"int",
|
||||||
|
"Automatische Abmeldung nach Leerlauf (Minuten)",
|
||||||
|
hint="Jede Anfrage setzt den Zaehler zurueck -- gezaehlt wird nur Untaetigkeit. "
|
||||||
|
"Gilt auch fuer die eingebundenen Dateifreigaben: mit der Sitzung endet die "
|
||||||
|
"Einbindung.",
|
||||||
|
section="system",
|
||||||
|
minimum=5,
|
||||||
|
maximum=1440,
|
||||||
|
),
|
||||||
SettingSpec("timezone", "", "str", "Zeitzone des Hosts", section="system"),
|
SettingSpec("timezone", "", "str", "Zeitzone des Hosts", section="system"),
|
||||||
SettingSpec("hostname", "", "str", "Hostname", section="system"),
|
SettingSpec("hostname", "", "str", "Hostname", section="system"),
|
||||||
SettingSpec(
|
SettingSpec(
|
||||||
@@ -117,7 +134,7 @@ def create_base_app(
|
|||||||
#: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die
|
#: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die
|
||||||
#: Funktionsliste von TESM waere dort schlicht falsch.
|
#: Funktionsliste von TESM waere dort schlicht falsch.
|
||||||
auth_tagline: str = "",
|
auth_tagline: str = "",
|
||||||
app_version: str = "2.0.0",
|
app_version: str = "2.0.2",
|
||||||
built_in_trusted_keys: tuple[str, ...] = (),
|
built_in_trusted_keys: tuple[str, ...] = (),
|
||||||
enable_license: bool = True,
|
enable_license: bool = True,
|
||||||
template_folder: str | None = None,
|
template_folder: str | None = None,
|
||||||
@@ -192,11 +209,25 @@ def create_base_app(
|
|||||||
)
|
)
|
||||||
app.extensions["tesm"] = extension
|
app.extensions["tesm"] = extension
|
||||||
|
|
||||||
|
def _leerlauf(conn: Any) -> int:
|
||||||
|
"""Leerlaufgrenze aus den Einstellungen -- ueber die offene Verbindung.
|
||||||
|
|
||||||
|
Bewusst mit der Verbindung, die die Sitzungsverwaltung schon haelt: eine
|
||||||
|
zweite zu oeffnen waere eine verschachtelte Datenbankverbindung, und die
|
||||||
|
gilt hier als Fehler.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
wert = int(settings.get(conn, "session_idle_minutes") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
wert = 0
|
||||||
|
return wert if wert > 0 else config.security.session_idle_minutes
|
||||||
|
|
||||||
app.session_interface = SqliteSessionInterface(
|
app.session_interface = SqliteSessionInterface(
|
||||||
database,
|
database,
|
||||||
cookie_name=app.config["SESSION_COOKIE_NAME"],
|
cookie_name=app.config["SESSION_COOKIE_NAME"],
|
||||||
idle_minutes=config.security.session_idle_minutes,
|
idle_minutes=config.security.session_idle_minutes,
|
||||||
absolute_hours=config.security.session_absolute_hours,
|
absolute_hours=config.security.session_absolute_hours,
|
||||||
|
idle_lookup=_leerlauf,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- Schema und Startzustand ------------------------------------------ #
|
# -- Schema und Startzustand ------------------------------------------ #
|
||||||
@@ -264,6 +295,7 @@ def create_base_app(
|
|||||||
"auth_tagline": auth_tagline,
|
"auth_tagline": auth_tagline,
|
||||||
"app_version": app_version,
|
"app_version": app_version,
|
||||||
"current_year": datetime.now(timezone.utc).year,
|
"current_year": datetime.now(timezone.utc).year,
|
||||||
|
"copyright_notice": COPYRIGHT_NOTICE,
|
||||||
"current_user": current_user,
|
"current_user": current_user,
|
||||||
"nav_items": nav_items,
|
"nav_items": nav_items,
|
||||||
"palette_entries": palette,
|
"palette_entries": palette,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ AAD_LDAP_BIND = "service_accounts.ldap_bind"
|
|||||||
|
|
||||||
#: userAccountControl-Bit fuer "Konto deaktiviert".
|
#: userAccountControl-Bit fuer "Konto deaktiviert".
|
||||||
ACCOUNTDISABLE = 0x0002
|
ACCOUNTDISABLE = 0x0002
|
||||||
#: OID fuer rekursive Gruppenauflösung in AD.
|
#: OID fuer rekursive Gruppenaufloesung in AD.
|
||||||
MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
|
MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
|
||||||
|
|
||||||
Status = Literal["ok", "invalid", "unavailable", "disabled"]
|
Status = Literal["ok", "invalid", "unavailable", "disabled"]
|
||||||
@@ -349,7 +349,7 @@ def _attr_int(entry: Any, name: str) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_groups(connection: Any, base: str, user_dn: str) -> tuple[str, ...]:
|
def _resolve_groups(connection: Any, base: str, user_dn: str) -> tuple[str, ...]:
|
||||||
"""Rekursive Gruppenauflösung ueber LDAP_MATCHING_RULE_IN_CHAIN."""
|
"""Rekursive Gruppenaufloesung ueber LDAP_MATCHING_RULE_IN_CHAIN."""
|
||||||
try:
|
try:
|
||||||
connection.search(
|
connection.search(
|
||||||
search_base=base,
|
search_base=base,
|
||||||
@@ -359,7 +359,7 @@ def _resolve_groups(connection: Any, base: str, user_dn: str) -> tuple[str, ...]
|
|||||||
)
|
)
|
||||||
return tuple(str(entry.entry_dn) for entry in connection.entries)
|
return tuple(str(entry.entry_dn) for entry in connection.entries)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
logger.warning("Gruppenauflösung fehlgeschlagen: %s", exc)
|
logger.warning("Gruppenaufloesung fehlgeschlagen: %s", exc)
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SecurityConfig:
|
|||||||
cookie_secure: bool = True
|
cookie_secure: bool = True
|
||||||
cookie_samesite: str = "Lax"
|
cookie_samesite: str = "Lax"
|
||||||
#: Leerlauf-Zeitfenster einer Sitzung.
|
#: Leerlauf-Zeitfenster einer Sitzung.
|
||||||
session_idle_minutes: int = 60
|
session_idle_minutes: int = 30
|
||||||
#: Harte Obergrenze unabhaengig von Aktivitaet.
|
#: Harte Obergrenze unabhaengig von Aktivitaet.
|
||||||
session_absolute_hours: int = 12
|
session_absolute_hours: int = 12
|
||||||
#: Wie lange eine erneute Passwortabfrage ("sudo-Modus") gilt.
|
#: Wie lange eine erneute Passwortabfrage ("sudo-Modus") gilt.
|
||||||
@@ -147,7 +147,7 @@ def load_core_config(
|
|||||||
security = SecurityConfig(
|
security = SecurityConfig(
|
||||||
cookie_secure=_env_bool(env_prefix, "COOKIE_SECURE", True),
|
cookie_secure=_env_bool(env_prefix, "COOKIE_SECURE", True),
|
||||||
cookie_samesite=_env(env_prefix, "COOKIE_SAMESITE") or "Lax",
|
cookie_samesite=_env(env_prefix, "COOKIE_SAMESITE") or "Lax",
|
||||||
session_idle_minutes=_env_int(env_prefix, "SESSION_IDLE_MINUTES", 60),
|
session_idle_minutes=_env_int(env_prefix, "SESSION_IDLE_MINUTES", 30),
|
||||||
session_absolute_hours=_env_int(env_prefix, "SESSION_ABSOLUTE_HOURS", 12),
|
session_absolute_hours=_env_int(env_prefix, "SESSION_ABSOLUTE_HOURS", 12),
|
||||||
reauth_valid_minutes=_env_int(env_prefix, "REAUTH_VALID_MINUTES", 15),
|
reauth_valid_minutes=_env_int(env_prefix, "REAUTH_VALID_MINUTES", 15),
|
||||||
login_max_attempts=_env_int(env_prefix, "LOGIN_MAX_ATTEMPTS", 8),
|
login_max_attempts=_env_int(env_prefix, "LOGIN_MAX_ATTEMPTS", 8),
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ SETTING_STATUS = "license_activation_status"
|
|||||||
SETTING_PENDING = "license_pending_request"
|
SETTING_PENDING = "license_pending_request"
|
||||||
SETTING_LAST_HEARTBEAT = "license_last_heartbeat_at"
|
SETTING_LAST_HEARTBEAT = "license_last_heartbeat_at"
|
||||||
SETTING_HEARTBEAT_ERROR = "license_heartbeat_error"
|
SETTING_HEARTBEAT_ERROR = "license_heartbeat_error"
|
||||||
|
|
||||||
|
#: Ab wann die Restlaufzeit als *Hinweis* erscheint (gruen). Unterhalb von
|
||||||
|
#: ``EXPIRY_WARNING_DAYS`` uebernimmt die Warnung (orange), nach Ablauf Rot.
|
||||||
|
EXPIRY_HINT_DAYS = 90
|
||||||
SETTING_EXPORT_GRACE = "license_export_grace"
|
SETTING_EXPORT_GRACE = "license_export_grace"
|
||||||
|
|
||||||
STATUS_INACTIVE = "inactive"
|
STATUS_INACTIVE = "inactive"
|
||||||
@@ -596,14 +600,22 @@ class LicenseManager:
|
|||||||
message = "Verbindung zum Lizenzserver bestaetigt."
|
message = "Verbindung zum Lizenzserver bestaetigt."
|
||||||
|
|
||||||
self.settings.set(conn, SETTING_PENDING, "", actor=actor)
|
self.settings.set(conn, SETTING_PENDING, "", actor=actor)
|
||||||
audit.log(
|
# Ein erfolgreicher Heartbeat laeuft alle paar Minuten und ist kein
|
||||||
conn,
|
# Ereignis, sondern der Normalzustand. Im Auditlog stand dadurch
|
||||||
actor=actor,
|
# praktisch nur noch er, und die Eintraege, um die es dort geht --
|
||||||
action=f"license.{action}",
|
# Aktivierung, Deaktivierung, Widerruf --, gingen darin unter. Der
|
||||||
target=str(request_payload["license_id"]),
|
# Zeitpunkt des letzten Heartbeats steht ohnehin in den Einstellungen
|
||||||
detail=f"Transport: {transport}",
|
# und auf der Lizenzseite. Ein *fehlgeschlagener* Heartbeat wird
|
||||||
severity="notice",
|
# weiterhin protokolliert (siehe die Fehlerpfade oben).
|
||||||
)
|
if action != "heartbeat":
|
||||||
|
audit.log(
|
||||||
|
conn,
|
||||||
|
actor=actor,
|
||||||
|
action=f"license.{action}",
|
||||||
|
target=str(request_payload["license_id"]),
|
||||||
|
detail=f"Transport: {transport}",
|
||||||
|
severity="notice",
|
||||||
|
)
|
||||||
self.reload(conn)
|
self.reload(conn)
|
||||||
return {"ok": True, "transport": transport, "code": "", "message": message}
|
return {"ok": True, "transport": transport, "code": "", "message": message}
|
||||||
|
|
||||||
@@ -704,6 +716,19 @@ class LicenseManager:
|
|||||||
except LicenseError as exc:
|
except LicenseError as exc:
|
||||||
with self.database.transaction(conn):
|
with self.database.transaction(conn):
|
||||||
self.settings.set(conn, SETTING_HEARTBEAT_ERROR, str(exc), actor="system")
|
self.settings.set(conn, SETTING_HEARTBEAT_ERROR, str(exc), actor="system")
|
||||||
|
# Der *erfolgreiche* Heartbeat wird nicht protokolliert -- er
|
||||||
|
# ist der Normalzustand und hat das Auditlog zugeschuettet.
|
||||||
|
# Ein Fehlschlag dagegen ist genau das, was dort stehen soll:
|
||||||
|
# er kann bedeuten, dass die Lizenz demnaechst in die
|
||||||
|
# Kulanzzeit laeuft.
|
||||||
|
audit.log(
|
||||||
|
conn,
|
||||||
|
actor="system",
|
||||||
|
action="license.heartbeat_failed",
|
||||||
|
target=self._state.license_id or "",
|
||||||
|
detail=str(exc)[:500],
|
||||||
|
severity="warning",
|
||||||
|
)
|
||||||
return {"ok": False, "message": str(exc)}
|
return {"ok": False, "message": str(exc)}
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -761,6 +786,17 @@ class LicenseManager:
|
|||||||
"dot_class": "status-pill__dot--pulse",
|
"dot_class": "status-pill__dot--pulse",
|
||||||
"title": "Bitte rechtzeitig verlaengern.",
|
"title": "Bitte rechtzeitig verlaengern.",
|
||||||
}
|
}
|
||||||
|
# Zwischen der Warnschwelle und 90 Tagen: gruen, rein als Hinweis. Wer
|
||||||
|
# eine Verlaengerung im Voraus plant, will das Datum sehen, ohne dass es
|
||||||
|
# nach einem Problem aussieht.
|
||||||
|
if status.days_left is not None and 0 < status.days_left <= EXPIRY_HINT_DAYS:
|
||||||
|
return {
|
||||||
|
"label": f"Lizenz laeuft in {status.days_left} Tagen ab",
|
||||||
|
"css_class": "",
|
||||||
|
"dot": "1",
|
||||||
|
"dot_class": "",
|
||||||
|
"title": "Nur ein Hinweis -- es ist nichts eingeschraenkt.",
|
||||||
|
}
|
||||||
if state.last_heartbeat_at:
|
if state.last_heartbeat_at:
|
||||||
try:
|
try:
|
||||||
last = lic.parse_iso(state.last_heartbeat_at)
|
last = lic.parse_iso(state.last_heartbeat_at)
|
||||||
|
|||||||
@@ -88,11 +88,24 @@ class SqliteSessionInterface(SessionInterface):
|
|||||||
cookie_name: str,
|
cookie_name: str,
|
||||||
idle_minutes: int,
|
idle_minutes: int,
|
||||||
absolute_hours: int,
|
absolute_hours: int,
|
||||||
|
idle_lookup: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.database = database
|
self.database = database
|
||||||
self.cookie_name = cookie_name
|
self.cookie_name = cookie_name
|
||||||
self.idle_minutes = idle_minutes
|
self.idle_minutes = idle_minutes
|
||||||
self.absolute_hours = absolute_hours
|
self.absolute_hours = absolute_hours
|
||||||
|
#: Optionale Nachschlagefunktion fuer die Leerlaufgrenze. Damit ist die
|
||||||
|
#: Zeit zur Laufzeit einstellbar, ohne den Dienst neu zu starten -- und
|
||||||
|
#: ohne eine zweite Datenbankverbindung, denn sie bekommt die offene.
|
||||||
|
self._idle_lookup = idle_lookup
|
||||||
|
|
||||||
|
def _idle(self, conn: Any) -> int:
|
||||||
|
if self._idle_lookup is None:
|
||||||
|
return self.idle_minutes
|
||||||
|
try:
|
||||||
|
return int(self._idle_lookup(conn)) or self.idle_minutes
|
||||||
|
except Exception: # pragma: no cover - Einstellung unlesbar
|
||||||
|
return self.idle_minutes
|
||||||
|
|
||||||
# -- Flask-Schnittstelle ------------------------------------------- #
|
# -- Flask-Schnittstelle ------------------------------------------- #
|
||||||
|
|
||||||
@@ -146,7 +159,7 @@ class SqliteSessionInterface(SessionInterface):
|
|||||||
with self.database.transaction(conn):
|
with self.database.transaction(conn):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE user_sessions SET last_seen_at=?, idle_expires_at=? WHERE id=?",
|
"UPDATE user_sessions SET last_seen_at=?, idle_expires_at=? WHERE id=?",
|
||||||
(_iso(now), _iso(now + timedelta(minutes=self.idle_minutes)), sid),
|
(_iso(now), _iso(now + timedelta(minutes=self._idle(conn))), sid),
|
||||||
)
|
)
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@@ -190,7 +203,7 @@ class SqliteSessionInterface(SessionInterface):
|
|||||||
session.get("user_id"),
|
session.get("user_id"),
|
||||||
_iso(now),
|
_iso(now),
|
||||||
_iso(now),
|
_iso(now),
|
||||||
_iso(now + timedelta(minutes=self.idle_minutes)),
|
_iso(now + timedelta(minutes=self._idle(conn))),
|
||||||
_iso(now + timedelta(hours=self.absolute_hours)),
|
_iso(now + timedelta(hours=self.absolute_hours)),
|
||||||
session.get("_ip", ""),
|
session.get("_ip", ""),
|
||||||
session.get("_ua", ""),
|
session.get("_ua", ""),
|
||||||
|
|||||||
@@ -408,6 +408,15 @@
|
|||||||
background: var(--danger-soft);
|
background: var(--danger-soft);
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
/* Gefuellt statt getoent: "kritisch" muss sich von "Warnung" unterscheiden,
|
||||||
|
und beide sind rot. */
|
||||||
|
.pill--critical {
|
||||||
|
background: var(--danger);
|
||||||
|
color: var(--surface-0);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.pill--critical::before { background: currentColor; }
|
||||||
|
|
||||||
.pill--warning, .pill--unknown, .pill--orange {
|
.pill--warning, .pill--unknown, .pill--orange {
|
||||||
background: var(--warning-soft);
|
background: var(--warning-soft);
|
||||||
color: var(--warning);
|
color: var(--warning);
|
||||||
@@ -436,6 +445,22 @@
|
|||||||
background: var(--success);
|
background: var(--success);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
/* Der Knopf sitzt *in* der Pille: kein Rahmen, keine Flaeche, nur das
|
||||||
|
Symbol in der Farbe der Pille. Ein Standardknopf wuerde sie sprengen. */
|
||||||
|
.status-pill__action { display: inline-flex; }
|
||||||
|
.status-pill__action button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.status-pill__action button:hover { opacity: 1; }
|
||||||
|
|
||||||
.status-pill__dot--pulse { animation: pulse-ring 2.4s var(--ease) infinite; }
|
.status-pill__dot--pulse { animation: pulse-ring 2.4s var(--ease) infinite; }
|
||||||
.status-pill--danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); color: var(--danger); }
|
.status-pill--danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); color: var(--danger); }
|
||||||
.status-pill--danger .status-pill__dot { background: var(--danger); }
|
.status-pill--danger .status-pill__dot { background: var(--danger); }
|
||||||
@@ -655,6 +680,37 @@
|
|||||||
}
|
}
|
||||||
.modal__title { font-size: var(--fs-md); font-weight: 640; flex: 1; }
|
.modal__title { font-size: var(--fs-md); font-weight: 640; flex: 1; }
|
||||||
.modal__sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 2px; }
|
.modal__sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 2px; }
|
||||||
|
/* Eine Datei-Vorschau soll die ganze Flaeche des Dialogs nutzen; der
|
||||||
|
uebliche Innenabstand des Dialogkoerpers wuerde sie einrahmen. */
|
||||||
|
.modal__body--flush { padding: 0; max-height: none; }
|
||||||
|
.modal__caption {
|
||||||
|
padding: 0 var(--sp-5) var(--sp-2);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.file-preview {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: min(70dvh, 760px);
|
||||||
|
border: 0;
|
||||||
|
background: var(--surface-0);
|
||||||
|
}
|
||||||
|
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
|
||||||
|
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
||||||
|
unruhig. */
|
||||||
|
.file-name {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--accent);
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.file-name:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.modal__body {
|
.modal__body {
|
||||||
padding: var(--sp-5);
|
padding: var(--sp-5);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -1035,6 +1091,16 @@
|
|||||||
.warning-text { color: var(--warning); }
|
.warning-text { color: var(--warning); }
|
||||||
.accent-text { color: var(--accent); }
|
.accent-text { color: var(--accent); }
|
||||||
.mono { font-family: var(--font-mono); font-size: 0.94em; }
|
.mono { font-family: var(--font-mono); font-size: 0.94em; }
|
||||||
|
/* Ein Inline-SVG ist im Reset display:block (base.css). In einem Knopf
|
||||||
|
stoert das nicht -- .btn ist inline-flex. In einem einfachen Link reisst
|
||||||
|
es die Beschriftung in die naechste Zeile. Fuer solche Links deshalb
|
||||||
|
ausdruecklich eine Zeile. */
|
||||||
|
.link-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.nowrap { white-space: nowrap; }
|
.nowrap { white-space: nowrap; }
|
||||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||||
.break-all { overflow-wrap: anywhere; }
|
.break-all { overflow-wrap: anywhere; }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function store(key, value) {
|
|||||||
if (value === null) localStorage.removeItem(key);
|
if (value === null) localStorage.removeItem(key);
|
||||||
else localStorage.setItem(key, value);
|
else localStorage.setItem(key, value);
|
||||||
} catch {
|
} catch {
|
||||||
/* Privatmodus o. Ä. -- Einstellungen sind dann nur fluechtig. */
|
/* Privatmodus o. Ae. -- Einstellungen sind dann nur fluechtig. */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,13 +212,21 @@ function initDialogs() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const dialog = document.getElementById(opener.dataset.dialogOpen);
|
const dialog = document.getElementById(opener.dataset.dialogOpen);
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
// Felder aus data-fill-* des Auslösers in das Formular uebernehmen.
|
// Felder aus data-fill-* des Ausloesers in das Formular uebernehmen.
|
||||||
Object.entries(opener.dataset).forEach(([key, value]) => {
|
Object.entries(opener.dataset).forEach(([key, value]) => {
|
||||||
if (!key.startsWith("fill")) return;
|
if (!key.startsWith("fill")) return;
|
||||||
const name = key.slice(4);
|
const name = key.slice(4);
|
||||||
const target = dialog.querySelector(`[data-fill-target="${name}"]`);
|
const target = dialog.querySelector(`[data-fill-target="${name}"]`);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
if ("value" in target) target.value = value;
|
// Wohin der Wert gehoert, haengt am Element: ein Rahmen will die
|
||||||
|
// Quelle, ein Link das Ziel, ein Feld einen Wert. Ohne diese
|
||||||
|
// Unterscheidung landete die URL als *Beschriftung* im
|
||||||
|
// Herunterladen-Knopf -- und weil das href dabei "#" blieb, lud ein
|
||||||
|
// Klick die Seite selbst als "Herunterladen.htm" herunter.
|
||||||
|
const als = target.dataset.fillAs;
|
||||||
|
if (als === "src") target.src = value;
|
||||||
|
else if (als === "href") target.href = value;
|
||||||
|
else if ("value" in target) target.value = value;
|
||||||
else target.textContent = value;
|
else target.textContent = value;
|
||||||
});
|
});
|
||||||
openDialog(dialog);
|
openDialog(dialog);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{# Auswahlfeld fuer eine Verzeichnisgruppe.
|
{# Auswahlfeld für eine Verzeichnisgruppe.
|
||||||
|
|
||||||
Der DN einer AD-Gruppe ist nichts, was man abtippt: er ist lang, enthaelt
|
Der DN einer AD-Gruppe ist nichts, was man abtippt: er ist lang, enthält
|
||||||
Kommas und Leerzeichen und ein Tippfehler faellt erst bei der naechsten
|
Kommas und Leerzeichen und ein Tippfehler fällt erst bei der nächsten
|
||||||
Anmeldung auf -- als stille Nicht-Zuordnung. Deshalb gesucht statt getippt.
|
Anmeldung auf -- als stille Nicht-Zuordnung. Deshalb gesucht statt getippt.
|
||||||
|
|
||||||
Das Suchergebnis kommt aus dem Verzeichnis selbst; die Felder werden dabei
|
Das Suchergebnis kommt aus dem Verzeichnis selbst; die Felder werden dabei
|
||||||
beide gefuellt (Anzeigename und DN). Getippt werden darf trotzdem: bei einer
|
beide gefüllt (Anzeigename und DN). Getippt werden darf trotzdem: bei einer
|
||||||
Umgebung ohne Dienstkonto ist die Suche nicht moeglich, und dann ist ein
|
Umgebung ohne Dienstkonto ist die Suche nicht möglich, und dann ist ein
|
||||||
eingefuegter DN besser als gar keine Zuordnung.
|
eingefügter DN besser als gar keine Zuordnung.
|
||||||
|
|
||||||
Erwartet: name_field, dn_field (Feldnamen), scope (eindeutiges Kuerzel),
|
Erwartet: name_field, dn_field (Feldnamen), scope (eindeutiges Kürzel),
|
||||||
optional name_value / dn_value und required.
|
optional name_value / dn_value und required.
|
||||||
#}
|
#}
|
||||||
{% macro group_picker(name_field, dn_field, scope, name_value='', dn_value='',
|
{% macro group_picker(name_field, dn_field, scope, name_value='', dn_value='',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
{% if current_user.is_directory_account %}
|
{% if current_user.is_directory_account %}
|
||||||
{{ alert('Ihre Stammdaten kommen aus dem Verzeichnisdienst und werden bei jeder
|
{{ alert('Ihre Stammdaten kommen aus dem Verzeichnisdienst und werden bei jeder
|
||||||
Anmeldung uebernommen.', 'info') }}
|
Anmeldung übernommen.', 'info') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{{ field('first_name', 'Vorname', value=profile.first_name,
|
{{ field('first_name', 'Vorname', value=profile.first_name,
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
{{ alert('Sie sind Administrator und duerfen unabhaengig von Gruppen alles.', 'warning') }}
|
{{ alert('Sie sind Administrator und dürfen unabhängig von Gruppen alles.', 'warning') }}
|
||||||
{% elif permissions %}
|
{% elif permissions %}
|
||||||
<div class="row row--tight">
|
<div class="row row--tight">
|
||||||
{% for permission in permissions %}
|
{% for permission in permissions %}
|
||||||
|
|||||||
@@ -35,26 +35,26 @@
|
|||||||
<div class="stack">
|
<div class="stack">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1"><div class="card__title">{{ icon('lock', size=17) }} Passwort aendern</div></div>
|
<div class="flex-1"><div class="card__title">{{ icon('lock', size=17) }} Passwort ändern</div></div>
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.is_directory_account %}
|
{% if current_user.is_directory_account %}
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
{{ alert('Ihr Passwort wird im Verzeichnisdienst gepflegt und dort geaendert.', 'info') }}
|
{{ alert('Ihr Passwort wird im Verzeichnisdienst gepflegt und dort geändert.', 'info') }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{{ url_for('account.change_password') }}">
|
<form method="post" action="{{ url_for('account.change_password') }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{{ field('current_password', 'Aktuelles Passwort', type='password', required=True,
|
{{ field('current_password', 'Aktülles Passwort', type='password', required=True,
|
||||||
autocomplete='current-password') }}
|
autocomplete='current-password') }}
|
||||||
{{ field('new_password', 'Neues Passwort', type='password', required=True,
|
{{ field('new_password', 'Neues Passwort', type='password', required=True,
|
||||||
autocomplete='new-password',
|
autocomplete='new-password',
|
||||||
hint_text='Mindestens ' ~ policy.min_length ~ ' Zeichen und zwei verschiedene Zeichenarten.') }}
|
hint_text='Mindestens ' ~ policy.min_length ~ ' Zeichen und zwei verschiedene Zeichenarten.') }}
|
||||||
{{ field('repeat_password', 'Neues Passwort wiederholen', type='password', required=True,
|
{{ field('repeat_password', 'Neues Passwort wiederholen', type='password', required=True,
|
||||||
autocomplete='new-password') }}
|
autocomplete='new-password') }}
|
||||||
{{ alert('Nach der Aenderung werden alle anderen Sitzungen beendet.', 'info') }}
|
{{ alert('Nach der Änderung werden alle anderen Sitzungen beendet.', 'info') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer"><button class="btn btn--primary" type="submit">Passwort aendern</button></div>
|
<div class="card__footer"><button class="btn btn--primary" type="submit">Passwort ändern</button></div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card__body stack">
|
<div class="card__body stack">
|
||||||
{% if pending_secret %}
|
{% if pending_secret %}
|
||||||
{{ alert('Richten Sie den Eintrag in Ihrer App ein und bestaetigen Sie mit einem aktuellen Code. Erst dann wird die Anmeldung umgestellt.', 'warning') }}
|
{{ alert('Richten Sie den Eintrag in Ihrer App ein und bestätigen Sie mit einem aktüllen Code. Erst dann wird die Anmeldung umgestellt.', 'warning') }}
|
||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
<dt>Geheimnis</dt>
|
<dt>Geheimnis</dt>
|
||||||
<dd><span class="mono" id="totp-secret">{{ pending_secret_grouped }}</span>
|
<dd><span class="mono" id="totp-secret">{{ pending_secret_grouped }}</span>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<input class="input input--mono" type="text" name="code" required maxlength="6"
|
<input class="input input--mono" type="text" name="code" required maxlength="6"
|
||||||
inputmode="numeric" placeholder="123456" style="max-width:130px;text-align:center">
|
inputmode="numeric" placeholder="123456" style="max-width:130px;text-align:center">
|
||||||
<button class="btn btn--primary" type="submit">Bestaetigen</button>
|
<button class="btn btn--primary" type="submit">Bestätigen</button>
|
||||||
</form>
|
</form>
|
||||||
{% elif totp_enabled %}
|
{% elif totp_enabled %}
|
||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<form method="post" action="{{ url_for('account.recovery_codes') }}" data-no-guard>
|
<form method="post" action="{{ url_for('account.recovery_codes') }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm" type="submit"
|
<button class="btn btn--sm" type="submit"
|
||||||
data-confirm="Neue Codes erzeugen? Die bisherigen verlieren ihre Gueltigkeit."
|
data-confirm="Neue Codes erzeugen? Die bisherigen verlieren ihre Gültigkeit."
|
||||||
data-confirm-title="Codes erneuern" data-confirm-ok="Erneuern" data-confirm-style="primary">
|
data-confirm-title="Codes erneuern" data-confirm-ok="Erneuern" data-confirm-style="primary">
|
||||||
Codes erneuern</button>
|
Codes erneuern</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -114,10 +114,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if require_totp_for_admins and current_user.is_admin %}
|
{% if require_totp_for_admins and current_user.is_admin %}
|
||||||
{{ alert('In dieser Installation ist ein zweiter Faktor fuer Administratoren verpflichtend. Bitte jetzt einrichten.', 'danger') }}
|
{{ alert('In dieser Installation ist ein zweiter Faktor für Administratoren verpflichtend. Bitte jetzt einrichten.', 'danger') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="muted fs-sm">
|
<p class="muted fs-sm">
|
||||||
Ein zweiter Faktor schuetzt Ihr Konto auch dann, wenn Ihr Passwort bekannt wird.
|
Ein zweiter Faktor schützt Ihr Konto auch dann, wenn Ihr Passwort bekannt wird.
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action="{{ url_for('account.totp_start') }}" data-no-guard>
|
<form method="post" action="{{ url_for('account.totp_start') }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{# ==========================================================================
|
{# ==========================================================================
|
||||||
Grundgeruest aller angemeldeten Seiten.
|
Grundgerüst aller angemeldeten Seiten.
|
||||||
Bloecke: extra_head, page_title, page_sub, breadcrumb, topbar_extra,
|
Blöcke: extra_head, page_title, page_sub, breadcrumb, topbar_extra,
|
||||||
content, dialogs, scripts
|
content, dialogs, scripts
|
||||||
========================================================================== #}
|
========================================================================== #}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
<aside class="sidebar" aria-label="Hauptnavigation">
|
<aside class="sidebar" aria-label="Hauptnavigation">
|
||||||
<div class="sidebar__brand">
|
<div class="sidebar__brand">
|
||||||
{# Die Wortmarke traegt den Namen selbst -- ein zweiter Textblock daneben
|
{# Die Wortmarke trägt den Namen selbst -- ein zweiter Textblock daneben
|
||||||
waere eine Doppelung. Eine einzige Zeichnung, deren Farben aus den
|
wäre eine Doppelung. Eine einzige Zeichnung, deren Farben aus den
|
||||||
Themen-Token kommen: zwei per Media Query versteckte SVG waeren bei
|
Themen-Token kommen: zwei per Media Query versteckte SVG wären bei
|
||||||
"System" und beim Umschalten von Hand falsch. #}
|
"System" und beim Umschalten von Hand falsch. #}
|
||||||
<span class="sidebar__logo" role="img" aria-label="{{ app_name }}">
|
<span class="sidebar__logo" role="img" aria-label="{{ app_name }}">
|
||||||
<svg viewBox="0 0 312 112" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 312 112" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -126,9 +126,25 @@
|
|||||||
<div class="topbar__right">
|
<div class="topbar__right">
|
||||||
{% block topbar_extra %}{% endblock %}
|
{% block topbar_extra %}{% endblock %}
|
||||||
{% for pill in status_pills %}
|
{% for pill in status_pills %}
|
||||||
|
{# Eine Pille kann mehr als Text sein: ein Countdown (data-countdown-to
|
||||||
|
wird laufend gezählt) und eine Aktion. Der Knopf steckt bewusst in
|
||||||
|
der Pille -- er gehört zu der Angabe, die er aktualisiert. #}
|
||||||
<span class="status-pill {{ pill.css_class }}" {% if pill.title %}title="{{ pill.title }}"{% endif %}>
|
<span class="status-pill {{ pill.css_class }}" {% if pill.title %}title="{{ pill.title }}"{% endif %}>
|
||||||
{% if pill.dot %}<span class="status-pill__dot {{ pill.dot_class }}"></span>{% endif %}
|
{% if pill.dot %}<span class="status-pill__dot {{ pill.dot_class }}"></span>{% endif %}
|
||||||
{{ pill.label }}
|
{{ pill.label }}
|
||||||
|
{% if pill.countdown_to %}
|
||||||
|
<span class="mono" data-countdown-to="{{ pill.countdown_to }}"
|
||||||
|
data-countdown-done="{{ pill.countdown_done or 'jetzt' }}"></span>
|
||||||
|
{% endif %}
|
||||||
|
{% if pill.suffix %}{{ pill.suffix }}{% endif %}
|
||||||
|
{% if pill.action %}
|
||||||
|
<form method="post" action="{{ pill.action }}" data-no-guard class="status-pill__action">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" title="{{ pill.action_title or 'Jetzt ausführen' }}"
|
||||||
|
aria-label="{{ pill.action_title or 'Jetzt ausführen' }}"
|
||||||
|
data-busy-label="...">{{ icon('refresh', size=13) }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button class="btn btn--icon btn--ghost hide-sm" type="button" data-behavior="open-palette"
|
<button class="btn btn--icon btn--ghost hide-sm" type="button" data-behavior="open-palette"
|
||||||
@@ -141,7 +157,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="page-footer">
|
<footer class="page-footer">
|
||||||
<span>{{ app_name }} {{ app_version }} · © {{ current_year }} WiS GmbH</span>
|
<span>{{ app_name }} {{ app_version }} · © {{ current_year }} {{ copyright_notice }}</span>
|
||||||
<span class="row row--tight">
|
<span class="row row--tight">
|
||||||
{% if migration_warning %}
|
{% if migration_warning %}
|
||||||
<a class="danger-text" href="{{ url_for('diagnostics.overview') }}">Schemawarnung</a>
|
<a class="danger-text" href="{{ url_for('diagnostics.overview') }}">Schemawarnung</a>
|
||||||
@@ -163,17 +179,17 @@
|
|||||||
aria-label="Suchbegriff" autocomplete="off">
|
aria-label="Suchbegriff" autocomplete="off">
|
||||||
<div class="palette__list"></div>
|
<div class="palette__list"></div>
|
||||||
<div class="palette__hint">
|
<div class="palette__hint">
|
||||||
<span><kbd>↑</kbd><kbd>↓</kbd> waehlen</span>
|
<span><kbd>↑</kbd><kbd>↓</kbd> wählen</span>
|
||||||
<span><kbd>Enter</kbd> oeffnen</span>
|
<span><kbd>Enter</kbd> öffnen</span>
|
||||||
<span><kbd>Esc</kbd> schliessen</span>
|
<span><kbd>Esc</kbd> schliessen</span>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
<script type="application/json" id="palette-data">{{ palette_entries | tojson }}</script>
|
<script type="application/json" id="palette-data">{{ palette_entries | tojson }}</script>
|
||||||
|
|
||||||
{# -- Bestaetigungsdialog (ersetzt window.confirm) ------------------------- #}
|
{# -- Bestätigungsdialog (ersetzt window.confirm) ------------------------- #}
|
||||||
<dialog class="modal modal--narrow" id="confirm-dialog" aria-labelledby="confirm-title">
|
<dialog class="modal modal--narrow" id="confirm-dialog" aria-labelledby="confirm-title">
|
||||||
<div class="modal__header">
|
<div class="modal__header">
|
||||||
<div class="modal__title" id="confirm-title" data-confirm-title>Bitte bestaetigen</div>
|
<div class="modal__title" id="confirm-title" data-confirm-title>Bitte bestätigen</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__body"><p data-confirm-text></p></div>
|
<div class="modal__body"><p data-confirm-text></p></div>
|
||||||
<div class="modal__footer">
|
<div class="modal__footer">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% from "tesm_core/macros.html" import alert, pill, stat %}
|
{% from "tesm_core/macros.html" import alert, pill, stat %}
|
||||||
|
|
||||||
{% block page_title %}Diagnose{% endblock %}
|
{% block page_title %}Diagnose{% endblock %}
|
||||||
{% block page_sub %}Schemastand, Protokollintegritaet, Schluessel und Systemzugriff{% endblock %}
|
{% block page_sub %}Schemastand, Protokollintegrität, Schlüssel und Systemzugriff{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid grid--4">
|
<div class="grid grid--4">
|
||||||
@@ -17,17 +17,17 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">
|
<div class="card__title">
|
||||||
{{ icon('shield', size=17) }} Aenderungsprotokoll
|
{{ icon('shield', size=17) }} Änderungsprotokoll
|
||||||
{% if chain.ok %}{{ pill('unversehrt', 'online') }}{% else %}{{ pill('Kette gebrochen', 'danger') }}{% endif %}
|
{% if chain.ok %}{{ pill('unversehrt', 'online') }}{% else %}{{ pill('Kette gebrochen', 'danger') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__sub">{{ chain.checked }} Zeilen geprueft (Hash-Kette)</div>
|
<div class="card__sub">{{ chain.checked }} Zeilen geprüft (Hash-Kette)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
{% if chain.ok %}
|
{% if chain.ok %}
|
||||||
<p class="muted fs-sm">
|
<p class="muted fs-sm">
|
||||||
Jede Zeile bindet den Hash ihrer Vorgaengerin ein. Eine nachtraeglich entfernte oder
|
Jede Zeile bindet den Hash ihrer Vorgängerin ein. Eine nachträglich entfernte oder
|
||||||
geaenderte Zeile wuerde hier auffallen.
|
geänderte Zeile würde hier auffallen.
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ alert(chain.reason ~ ' Erster Bruch bei Eintrag ' ~ chain.broken_at ~ '.', 'danger',
|
{{ alert(chain.reason ~ ' Erster Bruch bei Eintrag ' ~ chain.broken_at ~ '.', 'danger',
|
||||||
@@ -40,17 +40,17 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">
|
<div class="card__title">
|
||||||
{{ icon('key', size=17) }} Schluessel und Dateirechte
|
{{ icon('key', size=17) }} Schlüssel und Dateirechte
|
||||||
{% if key_problems %}{{ pill('Problem', 'danger') }}{% else %}{{ pill('in Ordnung', 'online') }}{% endif %}
|
{% if key_problems %}{{ pill('Problem', 'danger') }}{% else %}{{ pill('in Ordnung', 'online') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__sub">Aktiver Datenschluessel: {{ keystore_active }} (vorhanden: {{ keystore_keys | join(', ') }})</div>
|
<div class="card__sub">Aktiver Datenschlüssel: {{ keystore_active }} (vorhanden: {{ keystore_keys | join(', ') }})</div>
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
<form method="post" action="{{ url_for('diagnostics.rotate_keys') }}" data-no-guard>
|
<form method="post" action="{{ url_for('diagnostics.rotate_keys') }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm" type="submit"
|
<button class="btn btn--sm" type="submit"
|
||||||
data-confirm="Neuen Datenschluessel anlegen und gespeicherte Geheimnisse neu verschluesseln? Der alte Schluessel bleibt zum Lesen erhalten."
|
data-confirm="Neuen Datenschlüssel anlegen und gespeicherte Geheimnisse neu verschlüsseln? Der alte Schlüssel bleibt zum Lesen erhalten."
|
||||||
data-confirm-title="Schluessel rotieren" data-confirm-ok="Rotieren" data-confirm-style="primary">
|
data-confirm-title="Schlüssel rotieren" data-confirm-ok="Rotieren" data-confirm-style="primary">
|
||||||
{{ icon('refresh', size=14) }} Rotieren
|
{{ icon('refresh', size=14) }} Rotieren
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -63,8 +63,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="muted fs-sm">
|
<p class="muted fs-sm">
|
||||||
Alle Schluesseldateien liegen mit Modus 0600. Feldverschluesselung ist AES-256-GCM mit
|
Alle Schlüsseldateien liegen mit Modus 0600. Feldverschlüsselung ist AES-256-GCM mit
|
||||||
rotierbarem Schluesselbund; jedes Chiffrat ist an seinen Verwendungszweck gebunden.
|
rotierbarem Schlüsselbund; jedes Chiffrat ist an seinen Verwendungszweck gebunden.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<th class="sortable">Komponente</th>
|
<th class="sortable">Komponente</th>
|
||||||
<th class="sortable numeric">Version</th>
|
<th class="sortable numeric">Version</th>
|
||||||
<th class="sortable">Migration</th>
|
<th class="sortable">Migration</th>
|
||||||
<th>Pruefsumme</th>
|
<th>Prüfsumme</th>
|
||||||
<th class="sortable">Angewendet</th>
|
<th class="sortable">Angewendet</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">
|
<div class="card__title">
|
||||||
{{ icon('server', size=17) }} Systemzugriff
|
{{ icon('server', size=17) }} Systemzugriff
|
||||||
{% if not sysops_available %}{{ pill('nicht verfuegbar', 'warning') }}
|
{% if not sysops_available %}{{ pill('nicht verfügbar', 'warning') }}
|
||||||
{% elif sysops_selftest and sysops_selftest.ok %}{{ pill('Helfer antwortet', 'online') }}
|
{% elif sysops_selftest and sysops_selftest.ok %}{{ pill('Helfer antwortet', 'online') }}
|
||||||
{% else %}{{ pill('Helfer antwortet nicht', 'danger') }}{% endif %}
|
{% else %}{{ pill('Helfer antwortet nicht', 'danger') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<p class="muted fs-sm">
|
<p class="muted fs-sm">
|
||||||
Der Webprozess laeuft unprivilegiert. Alles, was Root-Rechte braucht, geht durch genau
|
Der Webprozess läuft unprivilegiert. Alles, was Root-Rechte braucht, geht durch genau
|
||||||
ein Helferskript mit Positivliste – nicht durch die Anwendung selbst.
|
ein Helferskript mit Positivliste – nicht durch die Anwendung selbst.
|
||||||
</p>
|
</p>
|
||||||
{% if not sysops_available %}
|
{% if not sysops_available %}
|
||||||
@@ -129,10 +129,10 @@
|
|||||||
Alles andere funktioniert normal -- das ist der erwartete Zustand in einer
|
Alles andere funktioniert normal -- das ist der erwartete Zustand in einer
|
||||||
Entwicklungsumgebung.', 'info') }}
|
Entwicklungsumgebung.', 'info') }}
|
||||||
{% elif sysops_selftest and not sysops_selftest.ok %}
|
{% elif sysops_selftest and not sysops_selftest.ok %}
|
||||||
{# Geprueft wird ein echter Aufruf, nicht die blosse Existenz der Datei.
|
{# Geprüft wird ein echter Aufruf, nicht die blosse Existenz der Datei.
|
||||||
Genau daran lag es einmal: die Seite meldete "erreichbar", waehrend
|
Genau daran lag es einmal: die Seite meldete "erreichbar", während
|
||||||
jeder Aufruf scheiterte. #}
|
jeder Aufruf scheiterte. #}
|
||||||
{{ alert(sysops_selftest.message or 'Der Helfer laesst sich nicht aufrufen.',
|
{{ alert(sysops_selftest.message or 'Der Helfer lässt sich nicht aufrufen.',
|
||||||
'danger', 'Systemaktionen funktionieren nicht') }}
|
'danger', 'Systemaktionen funktionieren nicht') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
<form method="post" action="{{ url_for('diagnostics.housekeeping') }}" data-no-guard>
|
<form method="post" action="{{ url_for('diagnostics.housekeeping') }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm" type="submit">{{ icon('refresh', size=14) }} Aufraeumen</button>
|
<button class="btn btn--sm" type="submit">{{ icon('refresh', size=14) }} Aufräumen</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -167,14 +167,14 @@
|
|||||||
{{ icon('key', size=17) }} Lizenzzustand
|
{{ icon('key', size=17) }} Lizenzzustand
|
||||||
{% if license_state.valid and license_state.activated %}{{ pill('aktiv', 'online') }}
|
{% if license_state.valid and license_state.activated %}{{ pill('aktiv', 'online') }}
|
||||||
{% elif license_state.valid %}{{ pill('nicht aktiviert', 'warning') }}
|
{% elif license_state.valid %}{{ pill('nicht aktiviert', 'warning') }}
|
||||||
{% else %}{{ pill('ungueltig', 'danger') }}{% endif %}
|
{% else %}{{ pill('ungültig', 'danger') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
<dt>Datei vorhanden</dt><dd>{{ license_state.present | yes_no }}</dd>
|
<dt>Datei vorhanden</dt><dd>{{ license_state.present | yes_no }}</dd>
|
||||||
<dt>Signatur gueltig</dt><dd>{{ license_state.valid | yes_no }}</dd>
|
<dt>Signatur gültig</dt><dd>{{ license_state.valid | yes_no }}</dd>
|
||||||
<dt>Aussteller gepinnt</dt><dd>{{ license_state.issuer_pinned | yes_no }}</dd>
|
<dt>Aussteller gepinnt</dt><dd>{{ license_state.issuer_pinned | yes_no }}</dd>
|
||||||
<dt>Rolle</dt><dd>{{ license_state.role }}</dd>
|
<dt>Rolle</dt><dd>{{ license_state.role }}</dd>
|
||||||
<dt>Module</dt><dd>{{ license_state.modules | sort | join(', ') or '–' | safe }}</dd>
|
<dt>Module</dt><dd>{{ license_state.modules | sort | join(', ') or '–' | safe }}</dd>
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
<div class="alert__body">{{ description }}</div>
|
<div class="alert__body">{{ description }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if request_id %}
|
{% if request_id %}
|
||||||
<p class="fs-xs faint center">Vorgangsnummer fuer die Fehlersuche: <span class="mono">{{ request_id }}</span></p>
|
<p class="fs-xs faint center">Vorgangsnummer für die Fehlersuche: <span class="mono">{{ request_id }}</span></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row row--end">
|
<div class="row row--end">
|
||||||
<a class="btn" href="{{ url_for('auth.login') if code in (401, 403) and not current_user.is_authenticated else back_url }}">
|
<a class="btn" href="{{ url_for('auth.login') if code in (401, 403) and not current_user.is_authenticated else back_url }}">
|
||||||
{{ 'Zur Anmeldung' if code in (401, 403) and not current_user.is_authenticated else 'Zurueck' }}
|
{{ 'Zur Anmeldung' if code in (401, 403) and not current_user.is_authenticated else 'Zurück' }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
{{ icon('lock', size=18) }}
|
{{ icon('lock', size=18) }}
|
||||||
<div class="alert__body">
|
<div class="alert__body">
|
||||||
<div class="alert__title">Systemgruppe</div>
|
<div class="alert__title">Systemgruppe</div>
|
||||||
Name und Rechte sind gesperrt. Anders als im Vorgaenger werden die Rechte
|
Name und Rechte sind gesperrt. Anders als im Vorgänger werden die Rechte
|
||||||
<strong>nicht</strong> bei jedem Neustart auf die Vorlage zurueckgesetzt –
|
<strong>nicht</strong> bei jedem Neustart auf die Vorlage zurückgesetzt –
|
||||||
Anpassungen bleiben erhalten.
|
Anpassungen bleiben erhalten.
|
||||||
{% if current_user.has('groups.edit') %}
|
{% if current_user.has('groups.edit') %}
|
||||||
<a class="btn btn--sm mt-2" href="{{ url_for('admin.group_detail', group_id=group.id, freischalten=1) }}">
|
<a class="btn btn--sm mt-2" href="{{ url_for('admin.group_detail', group_id=group.id, freischalten=1) }}">
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
{% if group.preset_key and current_user.is_admin %}
|
{% if group.preset_key and current_user.is_admin %}
|
||||||
<button class="btn btn--sm" type="submit"
|
<button class="btn btn--sm" type="submit"
|
||||||
formaction="{{ url_for('admin.group_apply_preset', group_id=group.id) }}"
|
formaction="{{ url_for('admin.group_apply_preset', group_id=group.id) }}"
|
||||||
data-confirm="Rechte dieser Gruppe auf die Vorlage zuruecksetzen? Bestehende Anpassungen gehen verloren."
|
data-confirm="Rechte dieser Gruppe auf die Vorlage zurücksetzen? Bestehende Anpassungen gehen verloren."
|
||||||
data-confirm-title="Vorlage anwenden" data-confirm-ok="Zuruecksetzen" data-confirm-style="primary">
|
data-confirm-title="Vorlage anwenden" data-confirm-ok="Zurücksetzen" data-confirm-style="primary">
|
||||||
{{ icon('refresh', size=14) }} Vorlage anwenden
|
{{ icon('refresh', size=14) }} Vorlage anwenden
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{{ field('name', 'Name', value=group.name, required=True, disabled=group.is_system,
|
{{ field('name', 'Name', value=group.name, required=True, disabled=group.is_system,
|
||||||
hint_text='Systemgruppen koennen nicht umbenannt werden.' if group.is_system else '') }}
|
hint_text='Systemgruppen können nicht umbenannt werden.' if group.is_system else '') }}
|
||||||
{{ textarea_field('description', 'Beschreibung', value=group.description, rows=2,
|
{{ textarea_field('description', 'Beschreibung', value=group.description, rows=2,
|
||||||
extra_class='field--full') }}
|
extra_class='field--full') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% from "tesm_core/macros.html" import field, textarea_field, pill, modal, alert, permission_matrix, empty_state %}
|
{% from "tesm_core/macros.html" import field, textarea_field, pill, modal, alert, permission_matrix, empty_state %}
|
||||||
|
|
||||||
{% block page_title %}Gruppen und Rechte{% endblock %}
|
{% block page_title %}Gruppen und Rechte{% endblock %}
|
||||||
{% block page_sub %}{{ groups | length }} Gruppen · Rechte addieren sich ueber alle Gruppen eines Benutzers{% endblock %}
|
{% block page_sub %}{{ groups | length }} Gruppen · Rechte addieren sich über alle Gruppen eines Benutzers{% endblock %}
|
||||||
|
|
||||||
{% block topbar_extra %}
|
{% block topbar_extra %}
|
||||||
{% if current_user.has('groups.create') %}
|
{% if current_user.has('groups.create') %}
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Wer selbst ein Recht nicht besitzt, kann es auch nicht vergeben. Diese Regel gilt fuer
|
{{ alert('Wer selbst ein Recht nicht besitzt, kann es auch nicht vergeben. Diese Regel gilt für
|
||||||
alle ausser echten Administratoren und verhindert, dass sich ein Gruppenverwalter ueber eine
|
alle ausser echten Administratoren und verhindert, dass sich ein Gruppenverwalter über eine
|
||||||
neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }}
|
neue Gruppe selbst höherstuft.', 'info', 'Eskalationsschutz') }}
|
||||||
|
|
||||||
<div class="grid grid--2">
|
<div class="grid grid--2">
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
@@ -37,9 +37,9 @@ neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }}
|
|||||||
<form method="post" action="{{ url_for('admin.group_delete', group_id=group.id) }}" data-no-guard>
|
<form method="post" action="{{ url_for('admin.group_delete', group_id=group.id) }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
data-confirm="Gruppe {{ group.name }} loeschen? Sie landet im Papierkorb; Mitglieder verlieren die zugehoerigen Rechte."
|
data-confirm="Gruppe {{ group.name }} löschen? Sie landet im Papierkorb; Mitglieder verlieren die zugehörigen Rechte."
|
||||||
data-confirm-title="Gruppe loeschen" data-confirm-ok="Loeschen"
|
data-confirm-title="Gruppe löschen" data-confirm-ok="Löschen"
|
||||||
aria-label="Loeschen">{{ icon('trash', size=15) }}</button>
|
aria-label="Löschen">{{ icon('trash', size=15) }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +67,7 @@ neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }}
|
|||||||
|
|
||||||
{% block dialogs %}
|
{% block dialogs %}
|
||||||
{% if current_user.has('groups.create') %}
|
{% if current_user.has('groups.create') %}
|
||||||
{% call modal('group-new', 'Gruppe anlegen', 'Rechte koennen anschliessend feiner eingestellt werden', 'wide') %}
|
{% call modal('group-new', 'Gruppe anlegen', 'Rechte können anschliessend feiner eingestellt werden', 'wide') %}
|
||||||
<form method="post" action="{{ url_for('admin.group_create') }}">
|
<form method="post" action="{{ url_for('admin.group_create') }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<div class="modal__body">
|
<div class="modal__body">
|
||||||
|
|||||||
@@ -24,6 +24,6 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if public_dashboard_url %}
|
{% if public_dashboard_url %}
|
||||||
<p class="center fs-sm"><a href="{{ public_dashboard_url }}">Zur oeffentlichen Statusuebersicht</a></p>
|
<p class="center fs-sm"><a href="{{ public_dashboard_url }}">Zur öffentlichen Statusübersicht</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% extends "tesm_core/base.html" %}
|
||||||
{% block page_title %}Archiv {{ day }}{% endblock %}
|
{% block page_title %}Archiv {{ day }}{% endblock %}
|
||||||
{% block page_sub %}{{ rows | length }} archivierte Aenderungen{% endblock %}
|
{% block page_sub %}{{ rows | length }} archivierte Änderungen{% endblock %}
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
<nav class="breadcrumb"><span><a href="{{ url_for('logs.history') }}">Verlauf</a></span><span>{{ day }}</span></nav>
|
<nav class="breadcrumb"><span><a href="{{ url_for('logs.history') }}">Verlauf</a></span><span>{{ day }}</span></nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% extends "tesm_core/base.html" %}
|
||||||
{% from "tesm_core/macros.html" import pill, empty_state, alert %}
|
{% from "tesm_core/macros.html" import pill, empty_state, alert %}
|
||||||
|
|
||||||
{% block page_title %}Aenderungsprotokoll{% endblock %}
|
{% block page_title %}Änderungsprotokoll{% endblock %}
|
||||||
{% block page_sub %}{{ total }} Eintraege · Hash-Kette {{ 'unversehrt' if chain.ok else 'gebrochen' }}{% endblock %}
|
{% block page_sub %}{{ total }} Einträge · Hash-Kette {{ 'unversehrt' if chain.ok else 'gebrochen' }}{% endblock %}
|
||||||
|
|
||||||
{% block topbar_extra %}
|
{% block topbar_extra %}
|
||||||
{% if current_user.has('logs_audit.export') %}
|
{% if current_user.has('logs_audit.export') %}
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% if not chain.ok %}
|
{% if not chain.ok %}
|
||||||
{{ alert(chain.reason ~ ' Erster Bruch bei Eintrag ' ~ chain.broken_at ~ '.', 'danger',
|
{{ alert(chain.reason ~ ' Erster Bruch bei Eintrag ' ~ chain.broken_at ~ '.', 'danger',
|
||||||
'Das Protokoll wurde veraendert') }}
|
'Das Protokoll wurde verändert') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ alert('Jeder Eintrag bindet den Hash seines Vorgaengers ein. Eine nachtraeglich entfernte
|
{{ alert('Jeder Eintrag bindet den Hash seines Vorgängers ein. Eine nachträglich entfernte
|
||||||
oder geaenderte Zeile faellt bei der Pruefung auf -- ' ~ chain.checked ~ ' Zeilen geprueft.',
|
oder geänderte Zeile fällt bei der Prüfung auf -- ' ~ chain.checked ~ ' Zeilen geprüft.',
|
||||||
'success') }}
|
'success') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<input class="input" type="text" name="benutzer" value="{{ filters.benutzer }}"
|
<input class="input" type="text" name="benutzer" value="{{ filters.benutzer }}"
|
||||||
placeholder="Benutzer" style="width:150px">
|
placeholder="Benutzer" style="width:150px">
|
||||||
<button class="btn btn--sm" type="submit">Filtern</button>
|
<button class="btn btn--sm" type="submit">Filtern</button>
|
||||||
<a class="btn btn--sm btn--ghost" href="{{ url_for('logs.audit_view') }}">Zuruecksetzen</a>
|
<a class="btn btn--sm btn--ghost" href="{{ url_for('logs.audit_view') }}">Zurücksetzen</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -66,10 +66,15 @@
|
|||||||
<td class="fs-sm">{{ row.target }}</td>
|
<td class="fs-sm">{{ row.target }}</td>
|
||||||
<td class="fs-sm muted break-all">{{ row.detail }}</td>
|
<td class="fs-sm muted break-all">{{ row.detail }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if row.severity == 'critical' %}{{ pill('kritisch', 'danger') }}
|
{# Ampel: Info grün, Hinweis gelb, Warnung rot. Vorher war Info
|
||||||
{% elif row.severity == 'warning' %}{{ pill('Warnung', 'warning') }}
|
grau und Hinweis blau -- dabei war nicht zu sehen, was harmlos
|
||||||
{% elif row.severity == 'notice' %}{{ pill('Hinweis', 'info') }}
|
ist und was Aufmerksamkeit braucht. "kritisch" bekommt eine
|
||||||
{% else %}{{ pill('Info', '') }}{% endif %}
|
eigene, gefüllte Darstellung, sonst wäre es von "Warnung"
|
||||||
|
nicht zu unterscheiden. #}
|
||||||
|
{% if row.severity == 'critical' %}{{ pill('kritisch', 'critical') }}
|
||||||
|
{% elif row.severity == 'warning' %}{{ pill('Warnung', 'danger') }}
|
||||||
|
{% elif row.severity == 'notice' %}{{ pill('Hinweis', 'warning') }}
|
||||||
|
{% else %}{{ pill('Info', 'online') }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="mono fs-xs faint">{{ row.ip or '–' | safe }}</td>
|
<td class="mono fs-xs faint">{{ row.ip or '–' | safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -77,13 +82,13 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if not rows %}{{ empty_state('Keine Eintraege', 'Zu diesem Filter gibt es nichts.', 'history') }}{% endif %}
|
{% if not rows %}{{ empty_state('Keine Einträge', 'Zu diesem Filter gibt es nichts.', 'history') }}{% endif %}
|
||||||
|
|
||||||
<div class="card__footer" style="justify-content:space-between">
|
<div class="card__footer" style="justify-content:space-between">
|
||||||
<span class="fs-sm muted">Seite {{ page }}</span>
|
<span class="fs-sm muted">Seite {{ page }}</span>
|
||||||
<span class="row row--tight">
|
<span class="row row--tight">
|
||||||
{% if page > 1 %}
|
{% if page > 1 %}
|
||||||
<a class="btn btn--sm" href="{{ url_for('logs.audit_view', seite=page-1, **filters) }}">Zurueck</a>
|
<a class="btn btn--sm" href="{{ url_for('logs.audit_view', seite=page-1, **filters) }}">Zurück</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if rows | length == per_page %}
|
{% if rows | length == per_page %}
|
||||||
<a class="btn btn--sm" href="{{ url_for('logs.audit_view', seite=page+1, **filters) }}">Weiter</a>
|
<a class="btn btn--sm" href="{{ url_for('logs.audit_view', seite=page+1, **filters) }}">Weiter</a>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Rotiert wird per logrotate mit copytruncate -- keiner der schreibenden Prozesse
|
{{ alert('Rotiert wird per logrotate mit copytruncate -- keiner der schreibenden Prozesse
|
||||||
(Ueberwachung, Web, Kea) kann ein Reopen-Signal verarbeiten.', 'info') }}
|
(Überwachung, Web, Kea) kann ein Reopen-Signal verarbeiten.', 'info') }}
|
||||||
|
|
||||||
<div class="grid grid--2">
|
<div class="grid grid--2">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{% if rotated %}
|
{% if rotated %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>Datei</th><th class="numeric">Groesse</th><th>Geaendert</th></tr></thead>
|
<thead><tr><th>Datei</th><th class="numeric">Grösse</th><th>Geändert</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in rotated %}
|
{% for entry in rotated %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('history', size=17) }} Archivierte Aenderungen</div>
|
<div class="card__title">{{ icon('history', size=17) }} Archivierte Änderungen</div>
|
||||||
<div class="card__sub">Aus der Datenbank ausgelagerte Kalendertage</div>
|
<div class="card__sub">Aus der Datenbank ausgelagerte Kalendertage</div>
|
||||||
</div>
|
</div>
|
||||||
{% if archived and current_user.has('logs_history.export') %}
|
{% if archived and current_user.has('logs_history.export') %}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
{% if archived %}
|
{% if archived %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>Tag</th><th class="numeric">Groesse</th><th></th></tr></thead>
|
<thead><tr><th>Tag</th><th class="numeric">Grösse</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in archived %}
|
{% for entry in archived %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
<form method="post" action="{{ url_for('logs.history_purge') }}" data-no-guard>
|
<form method="post" action="{{ url_for('logs.history_purge') }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
data-confirm="Alle {{ archived | length }} Archivdateien endgueltig vom Server loeschen? Bitte vorher als ZIP sichern."
|
data-confirm="Alle {{ archived | length }} Archivdateien endgültig vom Server löschen? Bitte vorher als ZIP sichern."
|
||||||
data-confirm-title="Archiv loeschen" data-confirm-ok="Loeschen">
|
data-confirm-title="Archiv löschen" data-confirm-ok="Löschen">
|
||||||
Archiv vom Server entfernen
|
Archiv vom Server entfernen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -51,8 +51,22 @@
|
|||||||
</label>
|
</label>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{# Ein nicht angehakter Haken sendet **nichts**. Wer die gesendeten Felder
|
||||||
|
auswertet, sieht ihn deshalb als "nicht angegeben" -- und liess den alten
|
||||||
|
Wert stehen: Haken raus, speichern, "keine Änderungen". Diese versteckte
|
||||||
|
Marke sagt "dieses Feld war im Formular", der Haken selbst sagt "an".
|
||||||
|
Damit sind alle drei Fälle unterscheidbar: an, aus, nicht Teil des
|
||||||
|
Formulars (Teilformulare dürfen fremde Schalter nicht löschen).
|
||||||
|
|
||||||
|
Kein ``disabled``-Fall: ein deaktivierter Haken darf auch nicht als "aus"
|
||||||
|
gespeichert werden, also fällt die Marke dann ebenfalls weg. #}
|
||||||
|
{% macro _present(name, disabled=False) -%}
|
||||||
|
{% if not disabled %}<input type="hidden" name="{{ name }}__present" value="1">{% endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro checkbox(name, label, checked=False, value='1', hint_text='', disabled=False) -%}
|
{% macro checkbox(name, label, checked=False, value='1', hint_text='', disabled=False) -%}
|
||||||
<label class="check {% if disabled %}check--disabled{% endif %}">
|
<label class="check {% if disabled %}check--disabled{% endif %}">
|
||||||
|
{{ _present(name, disabled) }}
|
||||||
<input type="checkbox" name="{{ name }}" value="{{ value }}"
|
<input type="checkbox" name="{{ name }}" value="{{ value }}"
|
||||||
{% if checked %}checked{% endif %} {% if disabled %}disabled{% endif %}>
|
{% if checked %}checked{% endif %} {% if disabled %}disabled{% endif %}>
|
||||||
<span>{{ label }} {{ hint(hint_text) }}</span>
|
<span>{{ label }} {{ hint(hint_text) }}</span>
|
||||||
@@ -61,6 +75,7 @@
|
|||||||
|
|
||||||
{% macro switch(name, label, checked=False, value='1', disabled=False) -%}
|
{% macro switch(name, label, checked=False, value='1', disabled=False) -%}
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
|
{{ _present(name, disabled) }}
|
||||||
<input type="checkbox" name="{{ name }}" value="{{ value }}"
|
<input type="checkbox" name="{{ name }}" value="{{ value }}"
|
||||||
{% if checked %}checked{% endif %} {% if disabled %}disabled{% endif %}>
|
{% if checked %}checked{% endif %} {% if disabled %}disabled{% endif %}>
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
@@ -196,12 +211,12 @@
|
|||||||
<span class="sr-only">{{ resource.label }} {{ action_meta[action][1] }}</span>
|
<span class="sr-only">{{ resource.label }} {{ action_meta[action][1] }}</span>
|
||||||
</label>
|
</label>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Bewusst *kein* Eingabefeld: ein deaktiviertes Kaestchen hat das
|
{# Bewusst *kein* Eingabefeld: ein deaktiviertes Kästchen hat das
|
||||||
JS beim Einschalten des Bereichs wieder freigegeben, es liess
|
JS beim Einschalten des Bereichs wieder freigegeben, es liess
|
||||||
sich anhaken, hatte aber kein name-Attribut -- es sah erteilt
|
sich anhaken, hatte aber kein name-Attribut -- es sah erteilt
|
||||||
aus und war nach dem Speichern weg. Was es nicht gibt, ist
|
aus und war nach dem Speichern weg. Was es nicht gibt, ist
|
||||||
hier deshalb gar kein Bedienelement. #}
|
hier deshalb gar kein Bedienelement. #}
|
||||||
<span class="matrix__na" title="Fuer diese Zeile nicht vorgesehen."></span>
|
<span class="matrix__na" title="Für diese Zeile nicht vorgesehen."></span>
|
||||||
<span class="sr-only">nicht vorgesehen</span>
|
<span class="sr-only">nicht vorgesehen</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@@ -216,7 +231,7 @@
|
|||||||
{% for action, meta in action_meta.items() %}
|
{% for action, meta in action_meta.items() %}
|
||||||
<span><b>{{ meta[0] }}</b> {{ meta[1] }} – {{ meta[2] }}</span>
|
<span><b>{{ meta[0] }}</b> {{ meta[1] }} – {{ meta[2] }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<span><span class="matrix__na"></span> Fuer diese Zeile nicht vorgesehen –
|
<span><span class="matrix__na"></span> Für diese Zeile nicht vorgesehen –
|
||||||
die Spalte gehoert zu einer anderen Zeile desselben Bereichs.</span>
|
die Spalte gehört zu einer anderen Zeile desselben Bereichs.</span>
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{% extends "tesm_core/standalone.html" %}
|
{% extends "tesm_core/standalone.html" %}
|
||||||
{% block heading %}Bitte bestaetigen{% endblock %}
|
{% block heading %}Bitte bestätigen{% endblock %}
|
||||||
{% block subheading %}Diese Aktion verlangt eine erneute Eingabe Ihres Passworts{% endblock %}
|
{% block subheading %}Diese Aktion verlangt eine erneute Eingabe Ihres Passworts{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="alert alert--info">
|
<div class="alert alert--info">
|
||||||
{{ icon('info', size=18) }}
|
{{ icon('info', size=18) }}
|
||||||
<div class="alert__body">
|
<div class="alert__body">
|
||||||
Sie greifen auf Geheimnisse oder einen Datenexport zu. Ein uebernommenes
|
Sie greifen auf Geheimnisse oder einen Datenexport zu. Ein übernommenes
|
||||||
Sitzungscookie allein reicht dafuer nicht aus – die Bestaetigung gilt
|
Sitzungscookie allein reicht dafür nicht aus – die Bestätigung gilt
|
||||||
danach {{ reauth_minutes }} Minuten.
|
danach {{ reauth_minutes }} Minuten.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="row row--end">
|
<div class="row row--end">
|
||||||
<a class="btn" href="{{ cancel_url }}">Abbrechen</a>
|
<a class="btn" href="{{ cancel_url }}">Abbrechen</a>
|
||||||
<button class="btn btn--primary" type="submit">Bestaetigen</button>
|
<button class="btn btn--primary" type="submit">Bestätigen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
inputmode="numeric" autocomplete="one-time-code" maxlength="14"
|
inputmode="numeric" autocomplete="one-time-code" maxlength="14"
|
||||||
placeholder="123456" style="letter-spacing:.22em;text-align:center;font-size:1.15rem">
|
placeholder="123456" style="letter-spacing:.22em;text-align:center;font-size:1.15rem">
|
||||||
<span class="field__hint">
|
<span class="field__hint">
|
||||||
Alternativ koennen Sie einen Ihrer Wiederherstellungscodes eingeben.
|
Alternativ können Sie einen Ihrer Wiederherstellungscodes eingeben.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn--primary btn--lg btn--block" type="submit">Bestaetigen</button>
|
<button class="btn btn--primary btn--lg btn--block" type="submit">Bestätigen</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="center fs-sm"><a href="{{ url_for('auth.logout_get') }}">Abbrechen und zurueck zur Anmeldung</a></p>
|
<p class="center fs-sm"><a href="{{ url_for('auth.logout_get') }}">Abbrechen und zurück zur Anmeldung</a></p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Sitzungen liegen in der Datenbank, nicht im Cookie. Ein widerrufener Eintrag ist sofort
|
{{ alert('Sitzungen liegen in der Datenbank, nicht im Cookie. Ein widerrufener Eintrag ist sofort
|
||||||
ungueltig -- auch wenn das Cookie noch existiert.', 'info') }}
|
ungültig -- auch wenn das Cookie noch existiert.', 'info') }}
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -17,8 +17,8 @@ ungueltig -- auch wenn das Cookie noch existiert.', 'info') }}
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="sortable">Benutzer</th>
|
<th class="sortable">Benutzer</th>
|
||||||
<th class="sortable">Angemeldet</th>
|
<th class="sortable">Angemeldet</th>
|
||||||
<th class="sortable">Letzte Aktivitaet</th>
|
<th class="sortable">Letzte Aktivität</th>
|
||||||
<th class="sortable">Laeuft ab</th>
|
<th class="sortable">Läuft ab</th>
|
||||||
<th class="sortable">IP</th>
|
<th class="sortable">IP</th>
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
<th class="actions">Aktion</th>
|
<th class="actions">Aktion</th>
|
||||||
|
|||||||
@@ -12,16 +12,16 @@
|
|||||||
{% if license_hint %}{{ alert(license_hint, 'warning') }}{% endif %}
|
{% if license_hint %}{{ alert(license_hint, 'warning') }}{% endif %}
|
||||||
|
|
||||||
{{ alert('Same Sign-On, nicht Single Sign-On: der Benutzer tippt sein Verzeichnispasswort auf der
|
{{ alert('Same Sign-On, nicht Single Sign-On: der Benutzer tippt sein Verzeichnispasswort auf der
|
||||||
normalen Anmeldeseite ein. Es wird bei jeder Anmeldung live gegen das Verzeichnis geprueft und
|
normalen Anmeldeseite ein. Es wird bei jeder Anmeldung live gegen das Verzeichnis geprüft und
|
||||||
niemals hier gespeichert.', 'info') }}
|
niemals hier gespeichert.', 'info') }}
|
||||||
|
|
||||||
{% if values.ldap_security == 'none' %}
|
{% if values.ldap_security == 'none' %}
|
||||||
{{ alert('Die Verbindung ist unverschluesselt. Das Passwort geht dabei im Klartext ueber das
|
{{ alert('Die Verbindung ist unverschlüsselt. Das Passwort geht dabei im Klartext über das
|
||||||
Netz -- nur fuer Testumgebungen vertretbar.', 'danger', 'Unverschluesselt') }}
|
Netz -- nur für Testumgebungen vertretbar.', 'danger', 'Unverschlüsselt') }}
|
||||||
{% elif not values.ldap_verify_cert %}
|
{% elif not values.ldap_verify_cert %}
|
||||||
{{ alert('Das Serverzertifikat wird nicht geprueft. Damit schuetzt die Verschluesselung nur
|
{{ alert('Das Serverzertifikat wird nicht geprüft. Damit schützt die Verschlüsselung nur
|
||||||
gegen passives Mitlesen, nicht gegen einen Man-in-the-Middle.', 'warning',
|
gegen passives Mitlesen, nicht gegen einen Man-in-the-Middle.', 'warning',
|
||||||
'Zertifikat ungeprueft') }}
|
'Zertifikat ungeprüft') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="grid grid--split">
|
<div class="grid grid--split">
|
||||||
@@ -48,8 +48,8 @@ niemals hier gespeichert.', 'info') }}
|
|||||||
{{ field('ldap_port', 'Port', value=values.ldap_port, type='number',
|
{{ field('ldap_port', 'Port', value=values.ldap_port, type='number',
|
||||||
disabled=bool(license_hint), attrs='id="ldap-port"') }}
|
disabled=bool(license_hint), attrs='id="ldap-port"') }}
|
||||||
{# Der Transport setzt den Port mit. Wer umstellt und den Port stehen
|
{# Der Transport setzt den Port mit. Wer umstellt und den Port stehen
|
||||||
laesst, bekommt eine Verbindung, die auf gut Glueck scheitert --
|
lässt, bekommt eine Verbindung, die auf gut Glück scheitert --
|
||||||
und die Meldung ("Zeitlimit") sagt nichts ueber die Ursache. Ein
|
und die Meldung ("Zeitlimit") sagt nichts über die Ursache. Ein
|
||||||
absichtlich abweichender Port bleibt erhalten. #}
|
absichtlich abweichender Port bleibt erhalten. #}
|
||||||
{{ select_field('ldap_security', 'Transport', specs['ldap_security'].choices,
|
{{ select_field('ldap_security', 'Transport', specs['ldap_security'].choices,
|
||||||
selected=values.ldap_security, disabled=bool(license_hint),
|
selected=values.ldap_security, disabled=bool(license_hint),
|
||||||
@@ -68,21 +68,21 @@ niemals hier gespeichert.', 'info') }}
|
|||||||
disabled=bool(license_hint)) }}
|
disabled=bool(license_hint)) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="stack stack--tight">
|
<div class="stack stack--tight">
|
||||||
{{ checkbox('ldap_verify_cert', 'Serverzertifikat pruefen', checked=values.ldap_verify_cert,
|
{{ checkbox('ldap_verify_cert', 'Serverzertifikat prüfen', checked=values.ldap_verify_cert,
|
||||||
disabled=bool(license_hint)) }}
|
disabled=bool(license_hint)) }}
|
||||||
{{ checkbox('ldap_group_sync', 'Gruppen aus dem Verzeichnis uebernehmen',
|
{{ checkbox('ldap_group_sync', 'Gruppen aus dem Verzeichnis übernehmen',
|
||||||
checked=values.ldap_group_sync, disabled=bool(license_hint)) }}
|
checked=values.ldap_group_sync, disabled=bool(license_hint)) }}
|
||||||
{{ checkbox('ldap_allow_insecure', 'Unverschluesselte Verbindung ausdruecklich erlauben',
|
{{ checkbox('ldap_allow_insecure', 'Unverschlüsselte Verbindung ausdrücklich erlauben',
|
||||||
checked=values.ldap_allow_insecure, disabled=bool(license_hint),
|
checked=values.ldap_allow_insecure, disabled=bool(license_hint),
|
||||||
hint_text=specs['ldap_allow_insecure'].hint) }}
|
hint_text=specs['ldap_allow_insecure'].hint) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend>Dienstkonto fuer die Suche (nur Lesezugriff noetig)</legend>
|
<legend>Dienstkonto für die Suche (nur Lesezugriff nötig)</legend>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{{ field('bind_username', 'Benutzer', value=bind_username, autocomplete='off',
|
{{ field('bind_username', 'Benutzer', value=bind_username, autocomplete='off',
|
||||||
disabled=not current_user.has('settings_directory.secrets')) }}
|
disabled=not current_user.has('settings_directory.secrets')) }}
|
||||||
{{ field('bind_password', 'Passwort (leer = unveraendert)', type='password',
|
{{ field('bind_password', 'Passwort (leer = unverändert)', type='password',
|
||||||
autocomplete='new-password',
|
autocomplete='new-password',
|
||||||
disabled=not current_user.has('settings_directory.secrets')) }}
|
disabled=not current_user.has('settings_directory.secrets')) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +125,7 @@ niemals hier gespeichert.', 'info') }}
|
|||||||
action="{{ url_for('hostsettings.directory_mapping_delete', mapping_id=entry.id) }}">
|
action="{{ url_for('hostsettings.directory_mapping_delete', mapping_id=entry.id) }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
data-confirm="Zuordnung entfernen? Betroffene Benutzer verlieren die Gruppe bei der naechsten Anmeldung."
|
data-confirm="Zuordnung entfernen? Betroffene Benutzer verlieren die Gruppe bei der nächsten Anmeldung."
|
||||||
data-confirm-title="Zuordnung entfernen" data-confirm-ok="Entfernen">
|
data-confirm-title="Zuordnung entfernen" data-confirm-ok="Entfernen">
|
||||||
{{ icon('trash', size=14) }}</button>
|
{{ icon('trash', size=14) }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,30 +5,30 @@
|
|||||||
{% block page_sub %}Backend: {{ backend }} · Sicherheitsnetz {{ revert_seconds }} s{% endblock %}
|
{% block page_sub %}Backend: {{ backend }} · Sicherheitsnetz {{ revert_seconds }} s{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Nach einer Aenderung laeuft ein Zeitgeber. Bestaetigen Sie nicht innerhalb von
|
{{ alert('Nach einer Änderung läuft ein Zeitgeber. Bestätigen Sie nicht innerhalb von
|
||||||
' ~ revert_seconds ~ ' Sekunden von der neuen Adresse aus, wird automatisch
|
' ~ revert_seconds ~ ' Sekunden von der neuen Adresse aus, wird automatisch
|
||||||
zurueckgerollt. So sperrt eine falsche Eingabe den Host nicht aus.', 'warning',
|
zurückgerollt. So sperrt eine falsche Eingabe den Host nicht aus.', 'warning',
|
||||||
'Automatischer Rueckfall') }}
|
'Automatischer Rückfall') }}
|
||||||
|
|
||||||
{% if pending %}
|
{% if pending %}
|
||||||
<section class="card card--danger">
|
<section class="card card--danger">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('alert', size=17) }} Aenderung wartet auf Bestaetigung</div>
|
<div class="card__title">{{ icon('alert', size=17) }} Änderung wartet auf Bestätigung</div>
|
||||||
<div class="card__sub">Interface {{ pending.interface }}</div>
|
<div class="card__sub">Interface {{ pending.interface }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<form method="post" action="{{ url_for('hostsettings.network_confirm') }}" data-no-guard>
|
<form method="post" action="{{ url_for('hostsettings.network_confirm') }}" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--primary" type="submit">Verbindung bestaetigen</button>
|
<button class="btn btn--primary" type="submit">Verbindung bestätigen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not sysops_available %}
|
{% if not sysops_available %}
|
||||||
{{ alert('Systemhelfer nicht verfuegbar -- die Anzeige ist rein lesend.', 'info') }}
|
{{ alert('Systemhelfer nicht verfügbar -- die Anzeige ist rein lesend.', 'info') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -54,7 +54,7 @@ zurueckgerollt. So sperrt eine falsche Eingabe den Host nicht aus.', 'warning',
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ empty_state('Keine Interfaces gelesen', 'Das System gibt hier nichts zurueck.', 'network') }}
|
{{ empty_state('Keine Interfaces gelesen', 'Das System gibt hier nichts zurück.', 'network') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -62,11 +62,11 @@ zurueckgerollt. So sperrt eine falsche Eingabe den Host nicht aus.', 'warning',
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('sliders', size=17) }} Aendern</div>
|
<div class="card__title">{{ icon('sliders', size=17) }} Ändern</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Es wird nur der Block des gewaehlten Interfaces geschrieben (Datei
|
Es wird nur der Block des gewählten Interfaces geschrieben (Datei
|
||||||
<span class="mono">{{ netplan_path }}</span>) – andere Interfaces bleiben
|
<span class="mono">{{ netplan_path }}</span>) – andere Interfaces bleiben
|
||||||
unberuehrt.
|
unberührt.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,18 +88,18 @@ zurueckgerollt. So sperrt eine falsche Eingabe den Host nicht aus.', 'warning',
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{{ field('address', 'IP-Adresse', placeholder='192.168.1.10') }}
|
{{ field('address', 'IP-Adresse', placeholder='192.168.1.10') }}
|
||||||
{{ field('prefix', 'Praefixlaenge', value='24', type='number') }}
|
{{ field('prefix', 'Präfixlänge', value='24', type='number') }}
|
||||||
{{ field('gateway', 'Gateway', placeholder='192.168.1.1') }}
|
{{ field('gateway', 'Gateway', placeholder='192.168.1.1') }}
|
||||||
{{ field('dns', 'DNS-Server', placeholder='192.168.1.1, 9.9.9.9') }}
|
{{ field('dns', 'DNS-Server', placeholder='192.168.1.1, 9.9.9.9') }}
|
||||||
</div>
|
</div>
|
||||||
{{ alert('Das Gateway muss im angegebenen Netz liegen -- sonst waere der Host nach der
|
{{ alert('Das Gateway muss im angegebenen Netz liegen -- sonst wäre der Host nach der
|
||||||
Aenderung nicht mehr erreichbar. Das wird vor dem Schreiben geprueft.', 'info') }}
|
Änderung nicht mehr erreichbar. Das wird vor dem Schreiben geprüft.', 'info') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
<button class="btn btn--danger" type="submit"
|
<button class="btn btn--danger" type="submit"
|
||||||
data-confirm="Netzwerkeinstellungen jetzt uebernehmen? Die Verbindung kann kurz abbrechen. Ohne Bestaetigung wird nach {{ revert_seconds }} Sekunden zurueckgerollt."
|
data-confirm="Netzwerkeinstellungen jetzt übernehmen? Die Verbindung kann kurz abbrechen. Ohne Bestätigung wird nach {{ revert_seconds }} Sekunden zurückgerollt."
|
||||||
data-confirm-title="Netzwerk aendern" data-confirm-ok="Uebernehmen">
|
data-confirm-title="Netzwerk ändern" data-confirm-ok="Übernehmen">
|
||||||
Uebernehmen
|
Übernehmen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% if license_hint %}{{ alert(license_hint, 'warning') }}{% endif %}
|
{% if license_hint %}{{ alert(license_hint, 'warning') }}{% endif %}
|
||||||
{% if not sysops_available %}
|
{% if not sysops_available %}
|
||||||
{{ alert('Systemhelfer nicht verfuegbar -- die Konfiguration kann hier erzeugt und angesehen,
|
{{ alert('Systemhelfer nicht verfügbar -- die Konfiguration kann hier erzeugt und angesehen,
|
||||||
aber nicht angewandt werden.', 'info') }}
|
aber nicht angewandt werden.', 'info') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ alert('Die Site-Datei wird bei jeder Uebernahme vollstaendig neu geschrieben.
|
{{ alert('Die Site-Datei wird bei jeder Übernahme vollständig neu geschrieben.
|
||||||
Alles, was ein Administrator sonst per Hand hineinschreiben wuerde, ist hier Einstellung. Der
|
Alles, was ein Administrator sonst per Hand hineinschreiben würde, ist hier Einstellung. Der
|
||||||
Vorgaenger schrieb die Datei nur, wenn sie noch nicht existierte -- ein einmal falscher Pfad
|
Vorgänger schrieb die Datei nur, wenn sie noch nicht existierte -- ein einmal falscher Pfad
|
||||||
blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.', 'info',
|
blieb dadurch über Jahre bestehen und lieferte alle statischen Dateien als 404.', 'info',
|
||||||
'Warum die Datei immer neu geschrieben wird') }}
|
'Warum die Datei immer neu geschrieben wird') }}
|
||||||
|
|
||||||
<div class="grid grid--split">
|
<div class="grid grid--split">
|
||||||
@@ -32,12 +32,12 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{{ field('web_domain', 'Domain', value=config.domain,
|
{{ field('web_domain', 'Domain', value=config.domain,
|
||||||
placeholder='tesm.firma.de', disabled=bool(license_hint),
|
placeholder='tesm.firma.de', disabled=bool(license_hint),
|
||||||
hint_text='Leer = Zugriff ueber IP-Adresse.') }}
|
hint_text='Leer = Zugriff über IP-Adresse.') }}
|
||||||
{{ field('web_acme_email', 'E-Mail fuer Let\'s Encrypt', value=acme_email, type='email',
|
{{ field('web_acme_email', 'E-Mail für Let\'s Encrypt', value=acme_email, type='email',
|
||||||
disabled=bool(license_hint)) }}
|
disabled=bool(license_hint)) }}
|
||||||
{{ field('web_cert_path', 'Zertifikat (Pfad)', value=config.cert_path,
|
{{ field('web_cert_path', 'Zertifikat (Pfad)', value=config.cert_path,
|
||||||
disabled=bool(license_hint), extra_class='field--full') }}
|
disabled=bool(license_hint), extra_class='field--full') }}
|
||||||
{{ field('web_key_path', 'Privater Schluessel (Pfad)', value=config.key_path,
|
{{ field('web_key_path', 'Privater Schlüssel (Pfad)', value=config.key_path,
|
||||||
disabled=bool(license_hint), extra_class='field--full') }}
|
disabled=bool(license_hint), extra_class='field--full') }}
|
||||||
{{ field('web_acme_webroot', 'ACME-Webroot', value=config.acme_webroot,
|
{{ field('web_acme_webroot', 'ACME-Webroot', value=config.acme_webroot,
|
||||||
disabled=bool(license_hint), extra_class='field--full') }}
|
disabled=bool(license_hint), extra_class='field--full') }}
|
||||||
@@ -47,7 +47,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
checked=config.https_enabled, disabled=bool(license_hint)) }}
|
checked=config.https_enabled, disabled=bool(license_hint)) }}
|
||||||
{{ checkbox('web_redirect_http', 'HTTP (Port 80) auf HTTPS umleiten',
|
{{ checkbox('web_redirect_http', 'HTTP (Port 80) auf HTTPS umleiten',
|
||||||
checked=config.redirect_http, disabled=bool(license_hint),
|
checked=config.redirect_http, disabled=bool(license_hint),
|
||||||
hint_text='Der ACME-Pfad bleibt dabei erreichbar -- sonst koennte Let\'s
|
hint_text='Der ACME-Pfad bleibt dabei erreichbar -- sonst könnte Let\'s
|
||||||
Encrypt nie erneuern.') }}
|
Encrypt nie erneuern.') }}
|
||||||
{{ checkbox('web_hsts_enabled', 'HSTS-Header senden',
|
{{ checkbox('web_hsts_enabled', 'HSTS-Header senden',
|
||||||
checked=config.hsts_enabled, disabled=bool(license_hint),
|
checked=config.hsts_enabled, disabled=bool(license_hint),
|
||||||
@@ -67,7 +67,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
<summary class="card__header" style="cursor:pointer">
|
<summary class="card__header" style="cursor:pointer">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('file-text', size=17) }} Erzeugte nginx-Konfiguration</div>
|
<div class="card__title">{{ icon('file-text', size=17) }} Erzeugte nginx-Konfiguration</div>
|
||||||
<div class="card__sub">Vorschau der Datei, die geschrieben wuerde</div>
|
<div class="card__sub">Vorschau der Datei, die geschrieben würde</div>
|
||||||
</div>
|
</div>
|
||||||
{{ icon('chevron-down', size=16) }}
|
{{ icon('chevron-down', size=16) }}
|
||||||
</summary>
|
</summary>
|
||||||
@@ -89,8 +89,8 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
{% if certificate.present and not certificate.get('error') %}
|
{% if certificate.present and not certificate.get('error') %}
|
||||||
<dl class="kv fs-sm">
|
<dl class="kv fs-sm">
|
||||||
<dt>Inhaber</dt><dd class="break-all">{{ certificate.subject }}</dd>
|
<dt>Inhaber</dt><dd class="break-all">{{ certificate.subject }}</dd>
|
||||||
<dt>Aussteller</dt><dd class="break-all">{{ certificate.issuer }}</dd>
|
<dt>Aussteller</dt><dd class="break-all">{{ certificate.issür }}</dd>
|
||||||
<dt>Gueltig bis</dt><dd>{{ certificate.not_valid_after | dt }}</dd>
|
<dt>Gültig bis</dt><dd>{{ certificate.not_valid_after | dt }}</dd>
|
||||||
<dt>Seriennummer</dt><dd class="mono fs-xs break-all">{{ certificate.serial }}</dd>
|
<dt>Seriennummer</dt><dd class="mono fs-xs break-all">{{ certificate.serial }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{% elif certificate.get('error') %}
|
{% elif certificate.get('error') %}
|
||||||
@@ -107,7 +107,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
<button class="btn btn--sm" type="submit"
|
<button class="btn btn--sm" type="submit"
|
||||||
{% if license_hint or not certbot_available or not config.domain %}disabled{% endif %}
|
{% if license_hint or not certbot_available or not config.domain %}disabled{% endif %}
|
||||||
data-busy-label="Fordere an ..."
|
data-busy-label="Fordere an ..."
|
||||||
data-confirm="Zertifikat fuer {{ config.domain }} per Let's Encrypt anfordern? Die Domain muss oeffentlich auf diesen Host zeigen."
|
data-confirm="Zertifikat für {{ config.domain }} per Let's Encrypt anfordern? Die Domain muss öffentlich auf diesen Host zeigen."
|
||||||
data-confirm-title="Zertifikat anfordern" data-confirm-ok="Anfordern"
|
data-confirm-title="Zertifikat anfordern" data-confirm-ok="Anfordern"
|
||||||
data-confirm-style="primary">
|
data-confirm-style="primary">
|
||||||
{{ icon('lock', size=14) }} Let's-Encrypt-Zertifikat anfordern
|
{{ icon('lock', size=14) }} Let's-Encrypt-Zertifikat anfordern
|
||||||
@@ -117,15 +117,15 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
<p class="fs-xs faint">certbot ist auf diesem Host nicht installiert.</p>
|
<p class="fs-xs faint">certbot ist auf diesem Host nicht installiert.</p>
|
||||||
{% elif not config.domain %}
|
{% elif not config.domain %}
|
||||||
<p class="fs-xs faint">
|
<p class="fs-xs faint">
|
||||||
Fuer Let's Encrypt wird oben eine Domain gebraucht, die oeffentlich auf
|
Für Let's Encrypt wird oben eine Domain gebraucht, die öffentlich auf
|
||||||
diesen Host zeigt.
|
diesen Host zeigt.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<p class="muted fs-sm">
|
<p class="muted fs-sm">
|
||||||
Kein oeffentlicher Name? Dann ein eigenes Zertifikat erzeugen. Browser
|
Kein öffentlicher Name? Dann ein eigenes Zertifikat erzeugen. Browser
|
||||||
zeigen dafuer eine Warnung, die Verbindung ist aber verschluesselt --
|
zeigen dafür eine Warnung, die Verbindung ist aber verschlüsselt --
|
||||||
und das Sitzungscookie darf endlich <span class="mono">Secure</span> tragen.
|
und das Sitzungscookie darf endlich <span class="mono">Secure</span> tragen.
|
||||||
</p>
|
</p>
|
||||||
{% if local_names %}
|
{% if local_names %}
|
||||||
@@ -133,7 +133,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
Wird ausgestellt auf:
|
Wird ausgestellt auf:
|
||||||
<span class="mono">{{ (config.domain or local_names[0]) }}</span>
|
<span class="mono">{{ (config.domain or local_names[0]) }}</span>
|
||||||
{% if local_names|length > 1 %}
|
{% if local_names|length > 1 %}
|
||||||
(zusaetzlich {{ local_names[1:] | join(', ') }})
|
(zusätzlich {{ local_names[1:] | join(', ') }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -156,7 +156,7 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
<section class="card card--accent">
|
<section class="card card--accent">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">{{ icon('upload', size=17) }} Uebernehmen</div>
|
<div class="card__title">{{ icon('upload', size=17) }} Übernehmen</div>
|
||||||
<div class="card__sub">Schreiben, <span class="mono">nginx -t</span>, dann neu laden</div>
|
<div class="card__sub">Schreiben, <span class="mono">nginx -t</span>, dann neu laden</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,11 +165,11 @@ blieb dadurch ueber Jahre bestehen und lieferte alle statischen Dateien als 404.
|
|||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--primary btn--block" type="submit"
|
<button class="btn btn--primary btn--block" type="submit"
|
||||||
{% if license_hint or not sysops_available %}disabled{% endif %}
|
{% if license_hint or not sysops_available %}disabled{% endif %}
|
||||||
data-busy-label="Uebernehme ..."
|
data-busy-label="Übernehme ..."
|
||||||
data-confirm="Konfiguration schreiben, pruefen und nginx neu laden? Eine fehlerhafte Konfiguration wird vorher abgewiesen."
|
data-confirm="Konfiguration schreiben, prüfen und nginx neu laden? Eine fehlerhafte Konfiguration wird vorher abgewiesen."
|
||||||
data-confirm-title="nginx uebernehmen" data-confirm-ok="Uebernehmen"
|
data-confirm-title="nginx übernehmen" data-confirm-ok="Übernehmen"
|
||||||
data-confirm-style="primary">
|
data-confirm-style="primary">
|
||||||
Konfiguration uebernehmen
|
Konfiguration übernehmen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{# ==========================================================================
|
{# ==========================================================================
|
||||||
Grundgeruest fuer Seiten ohne Anwendungsrahmen: Anmeldung, zweiter Faktor,
|
Grundgerüst für Seiten ohne Anwendungsrahmen: Anmeldung, zweiter Faktor,
|
||||||
Kundenportal, Self-Service.
|
Kundenportal, Self-Service.
|
||||||
|
|
||||||
Wichtiger Unterschied zu base.html: Flash-Meldungen werden hier **sichtbar
|
Wichtiger Unterschied zu base.html: Flash-Meldungen werden hier **sichtbar
|
||||||
serverseitig** gerendert, nicht ueber die JSON-Bruecke. Der Vorgaenger hatte
|
serverseitig** gerendert, nicht über die JSON-Brücke. Der Vorgänger hatte
|
||||||
dieselbe Trennung; sie ist wichtig, weil diese Seiten auch ohne JavaScript
|
dieselbe Trennung; sie ist wichtig, weil diese Seiten auch ohne JavaScript
|
||||||
nutzbar bleiben muessen (und weil Tests hier direkt auf Text pruefen).
|
nutzbar bleiben müssen (und weil Tests hier direkt auf Text prüfen).
|
||||||
========================================================================== #}
|
========================================================================== #}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
<body data-csrf="{{ csrf_token() }}">
|
<body data-csrf="{{ csrf_token() }}">
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
{% if auth_tagline %}
|
{% if auth_tagline %}
|
||||||
{# Wortmarke mit Funktionszeile als tatsaechlicher Seitenhintergrund --
|
{# Wortmarke mit Funktionszeile als tatsächlicher Seitenhintergrund --
|
||||||
uebernommen vom Vorgaenger. Bewusst inline und mit Themen-Tokens statt
|
übernommen vom Vorgänger. Bewusst inline und mit Themen-Tokens statt
|
||||||
zweier Dateien fuer hell und dunkel: bei der Einstellung "System" und
|
zweier Dateien für hell und dunkel: bei der Einstellung "System" und
|
||||||
beim Umschalten von Hand waere die versteckte Variante die falsche.
|
beim Umschalten von Hand wäre die versteckte Variante die falsche.
|
||||||
``aria-hidden``, weil der Name schon in der Ueberschrift steht. #}
|
``aria-hidden``, weil der Name schon in der Überschrift steht. #}
|
||||||
<div class="auth-backdrop" aria-hidden="true">
|
<div class="auth-backdrop" aria-hidden="true">
|
||||||
<svg viewBox="0 0 620 190" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 620 190" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="translate(150,15)">
|
<g transform="translate(150,15)">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<p class="auth-footnote">{{ app_name }} {{ app_version }} · © {{ current_year }} WiS GmbH</p>
|
<p class="auth-footnote">{{ app_name }} {{ app_version }} · © {{ current_year }} {{ copyright_notice }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="{{ asset_url('js/app.js') }}" nonce="{{ csp_nonce }}"></script>
|
<script type="module" src="{{ asset_url('js/app.js') }}" nonce="{{ csp_nonce }}"></script>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
{% from "tesm_core/macros.html" import empty_state, alert, pill %}
|
{% from "tesm_core/macros.html" import empty_state, alert, pill %}
|
||||||
|
|
||||||
{% block page_title %}Papierkorb{% endblock %}
|
{% block page_title %}Papierkorb{% endblock %}
|
||||||
{% block page_sub %}{{ total }} Eintraege · Aufbewahrung {{ retention_days }} Tage{% endblock %}
|
{% block page_sub %}{{ total }} Einträge · Aufbewahrung {{ retention_days }} Tage{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Ansehen ist immer moeglich. Wiederherstellen und endgueltiges Loeschen erfordern eine
|
{{ alert('Ansehen ist immer möglich. Wiederherstellen und endgültiges Löschen erfordern eine
|
||||||
gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Seite entfernt.',
|
gültige, aktivierte Lizenz. Abgelaufene Einträge werden beim Aufruf dieser Seite entfernt.',
|
||||||
'info') }}
|
'info') }}
|
||||||
|
|
||||||
{% for section in sections %}
|
{% for section in sections %}
|
||||||
@@ -23,8 +23,8 @@ gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Sei
|
|||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" name="kind" value="{{ section.key }}">
|
<input type="hidden" name="kind" value="{{ section.key }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
data-confirm="Alle {{ section.entries | length }} Eintraege unter {{ section.label }} endgueltig loeschen? Das laesst sich nicht rueckgaengig machen."
|
data-confirm="Alle {{ section.entries | length }} Einträge unter {{ section.label }} endgültig löschen? Das lässt sich nicht rückgängig machen."
|
||||||
data-confirm-title="Endgueltig loeschen" data-confirm-ok="Endgueltig loeschen">
|
data-confirm-title="Endgültig löschen" data-confirm-ok="Endgültig löschen">
|
||||||
Bereich leeren
|
Bereich leeren
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -33,7 +33,7 @@ gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Sei
|
|||||||
{% if section.entries %}
|
{% if section.entries %}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>Eintrag</th><th>Detail</th><th>Geloescht</th><th class="actions">Aktion</th></tr></thead>
|
<thead><tr><th>Eintrag</th><th>Detail</th><th>Gelöscht</th><th class="actions">Aktion</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in section.entries %}
|
{% for entry in section.entries %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -53,9 +53,9 @@ gueltige, aktivierte Lizenz. Abgelaufene Eintraege werden beim Aufruf dieser Sei
|
|||||||
action="{{ url_for('trash.purge', kind=section.key, ident=entry.id) }}">
|
action="{{ url_for('trash.purge', kind=section.key, ident=entry.id) }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
data-confirm="{{ entry.label }} endgueltig loeschen?"
|
data-confirm="{{ entry.label }} endgültig löschen?"
|
||||||
data-confirm-title="Endgueltig loeschen" data-confirm-ok="Loeschen">
|
data-confirm-title="Endgültig löschen" data-confirm-ok="Löschen">
|
||||||
Endgueltig
|
Endgültig
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<span class="table-toolbar__spacer"></span>
|
<span class="table-toolbar__spacer"></span>
|
||||||
{% if directory_enabled and current_user.has('users.create') %}
|
{% if directory_enabled and current_user.has('users.create') %}
|
||||||
<button class="btn btn--sm" type="button" data-dialog-open="user-directory">
|
<button class="btn btn--sm" type="button" data-dialog-open="user-directory">
|
||||||
{{ icon('users', size=15) }} Aus Verzeichnis uebernehmen
|
{{ icon('users', size=15) }} Aus Verzeichnis übernehmen
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
@@ -90,9 +90,9 @@
|
|||||||
style="display:inline" data-no-guard>
|
style="display:inline" data-no-guard>
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||||
data-confirm="Konto {{ user.username }} wirklich loeschen?{% if user.auth_source == 'ldap' %} Verzeichniskonten werden sofort endgueltig entfernt.{% else %} Es landet im Papierkorb.{% endif %}"
|
data-confirm="Konto {{ user.username }} wirklich löschen?{% if user.auth_source == 'ldap' %} Verzeichniskonten werden sofort endgültig entfernt.{% else %} Es landet im Papierkorb.{% endif %}"
|
||||||
data-confirm-title="Konto loeschen" data-confirm-ok="Loeschen"
|
data-confirm-title="Konto löschen" data-confirm-ok="Löschen"
|
||||||
aria-label="Loeschen">{{ icon('trash', size=15) }}</button>
|
aria-label="Löschen">{{ icon('trash', size=15) }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@@ -106,8 +106,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{ alert('Rechte werden ausschliesslich ueber Gruppen vergeben. Das Admin-Kennzeichen ist davon
|
{{ alert('Rechte werden ausschliesslich über Gruppen vergeben. Das Admin-Kennzeichen ist davon
|
||||||
unabhaengig und kann nur von Administratoren gesetzt werden; mindestens ein entsperrtes
|
unabhängig und kann nur von Administratoren gesetzt werden; mindestens ein entsperrtes
|
||||||
Administratorkonto muss immer bestehen bleiben.', 'info') }}
|
Administratorkonto muss immer bestehen bleiben.', 'info') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }}
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
{{ checkbox('is_admin', 'Administrator (darf alles, unabhaengig von Gruppen)') }}
|
{{ checkbox('is_admin', 'Administrator (darf alles, unabhängig von Gruppen)') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ checkbox('must_change_password', 'Passwortwechsel bei der ersten Anmeldung erzwingen', checked=True) }}
|
{{ checkbox('must_change_password', 'Passwortwechsel bei der ersten Anmeldung erzwingen', checked=True) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +149,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }}
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% if directory_enabled %}
|
{% if directory_enabled %}
|
||||||
{% call modal('user-directory', 'Aus Verzeichnis uebernehmen',
|
{% call modal('user-directory', 'Aus Verzeichnis übernehmen',
|
||||||
'Konto vorbereiten, damit Gruppen vor der ersten Anmeldung sitzen') %}
|
'Konto vorbereiten, damit Gruppen vor der ersten Anmeldung sitzen') %}
|
||||||
<form method="post" action="{{ url_for('admin.directory_adopt') }}">
|
<form method="post" action="{{ url_for('admin.directory_adopt') }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
@@ -169,7 +169,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }}
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{{ alert('Name und E-Mail werden bei der ersten echten Anmeldung aus dem Verzeichnis
|
{{ alert('Name und E-Mail werden bei der ersten echten Anmeldung aus dem Verzeichnis
|
||||||
uebernommen und ueberschrieben.', 'info') }}
|
übernommen und überschrieben.', 'info') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__footer">
|
<div class="modal__footer">
|
||||||
<button class="btn" type="button" data-dialog-close>Abbrechen</button>
|
<button class="btn" type="button" data-dialog-close>Abbrechen</button>
|
||||||
@@ -198,7 +198,7 @@ Administratorkonto muss immer bestehen bleiben.', 'info') }}
|
|||||||
{{ field('email', 'E-Mail', value=user.email, type='email') }}
|
{{ field('email', 'E-Mail', value=user.email, type='email') }}
|
||||||
{{ field('new_password', 'Neues Passwort setzen (optional)', type='password',
|
{{ field('new_password', 'Neues Passwort setzen (optional)', type='password',
|
||||||
autocomplete='new-password',
|
autocomplete='new-password',
|
||||||
hint_text='Setzt das Passwort zurueck, erzwingt einen Wechsel und beendet alle Sitzungen des Kontos.') }}
|
hint_text='Setzt das Passwort zurück, erzwingt einen Wechsel und beendet alle Sitzungen des Kontos.') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend>Gruppen</legend>
|
<legend>Gruppen</legend>
|
||||||
|
|||||||
@@ -126,6 +126,37 @@ def format_date(value: Any) -> str:
|
|||||||
return format_datetime(value, with_time=False)
|
return format_datetime(value, with_time=False)
|
||||||
|
|
||||||
|
|
||||||
|
def format_millis(value: Any) -> str:
|
||||||
|
"""Millisekunden lesbar: ms, dann Sekunden, dann Minuten:Sekunden.
|
||||||
|
|
||||||
|
Vierzehn Stellen Millisekunden liest niemand. "315895 ms" sagt nichts,
|
||||||
|
"5:15" sofort etwas. Die Grenzen liegen dort, wo die naechstgroessere
|
||||||
|
Einheit anfaengt, und die Sekunden bekommen zwei Nachkommastellen -- bei
|
||||||
|
einem Neustart ist der Unterschied zwischen 15,2 s und 15,97 s durchaus
|
||||||
|
von Interesse.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
millis = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "-"
|
||||||
|
if millis < 0:
|
||||||
|
return "-"
|
||||||
|
# Unter 10 ms mit einer Nachkommastelle: Antwortzeiten liegen oft bei
|
||||||
|
# 0,3 ms, und "0 ms" waere schlicht falsch.
|
||||||
|
if millis < 10:
|
||||||
|
return f"{millis:.1f} ms".replace(".", ",")
|
||||||
|
if millis < 1000:
|
||||||
|
return f"{int(round(millis))} ms"
|
||||||
|
# Erst runden, dann die Einheit waehlen: 59999 ms sind gerundet 60,00 s und
|
||||||
|
# gehoeren damit in die Minutenform, nicht in eine Sekundenangabe, die wie
|
||||||
|
# eine Minute aussieht.
|
||||||
|
hundertstel = int(round(millis / 10))
|
||||||
|
if hundertstel < 60 * 100:
|
||||||
|
return f"{hundertstel / 100:.2f} s".replace(".", ",")
|
||||||
|
minuten, rest = divmod(hundertstel, 60 * 100)
|
||||||
|
return f"{minuten}:{rest / 100:05.2f}".replace(".", ",")
|
||||||
|
|
||||||
|
|
||||||
def format_duration(seconds: Any) -> str:
|
def format_duration(seconds: Any) -> str:
|
||||||
try:
|
try:
|
||||||
total = int(seconds)
|
total = int(seconds)
|
||||||
@@ -256,6 +287,7 @@ def init_app(app: Flask) -> None:
|
|||||||
dt=format_datetime,
|
dt=format_datetime,
|
||||||
date=format_date,
|
date=format_date,
|
||||||
duration=format_duration,
|
duration=format_duration,
|
||||||
|
millis=format_millis,
|
||||||
number=format_number,
|
number=format_number,
|
||||||
yes_no=yes_no,
|
yes_no=yes_no,
|
||||||
from_json=from_json,
|
from_json=from_json,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing-server"
|
name = "tesm-licensing-server"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten"
|
description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["tesm-licensing==2.0.0", "cryptography>=41"]
|
dependencies = ["tesm-licensing==2.0.2", "cryptography>=41"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["cryptography>=42"]
|
dependencies = ["cryptography>=42"]
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ from .protocol import (
|
|||||||
verify_master_response,
|
verify_master_response,
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.0.2"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ACTIONS",
|
"ACTIONS",
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user