Fork von TESM (srv/tesm), auf Kunden-/Lizenzverwaltung reduziert statt
Geraete-/PoE-Management. Wiederverwendet unveraendert: Login/Session/
Benutzer- und Gruppenverwaltung, LDAP/AD, NGINX- und Systemeinstellungen,
Live-Log/Verlauf/Auditlog, Im-/Export-Grundgeruest, das komplette
PERMISSIONS/NAV_ITEMS/inject_nav()-Rechtesystem sowie das in Phase 1
gebaute Lizenzsystem selbst sowohl fuer die MASTER-eigene Bootstrap-Lizenz
als auch fuer das exakt gleiche licensing.py (byte-identisch zu TESM --
Signieren/Verifizieren muss zwischen beiden Seiten kompatibel bleiben).
Entfernt: Clients/Switche/Zugangsdaten/DHCP/Fileshare/Wartung/Papierkorb/
manueller PoE-Neustart/SSH-Terminal inkl. aller zugehoerigen Tabellen,
Routen, Permissions, Nav-Eintraege und Abhaengigkeiten (paramiko/Flask-Sock/
simple-websocket/PyNaCl/pyasn1 aus requirements.txt). Benutzer-/Gruppen-
Loeschung von Soft- auf Hard-Delete umgestellt (kein Papierkorb mehr).
Eigener Pfad-/Env-Var-Namespace (TESM_LICENSE_* statt TESM_*, /var/log/
tesm-license statt /var/log/tesm usw.), damit Master und TESM testweise
sogar auf demselben Host nebeneinander laufen koennen, ohne sich Log-/
Config-Pfade streitig zu machen.
Neu -- der eigentliche Lizenzserver:
- license_customers/licenses-Tabellen, Master-Signaturschluessel
(master_signing_key.json, einmalig erzeugt, NIE automatisch rotiert --
jede Kundenlizenz traegt den zum Ausstellungszeitpunkt aktuellen
master_pubkey fest eingebettet).
- Dashboard ('/') als Kunden-/Lizenzuebersicht (Typ, Module, Ablauf,
Status, letzter Heartbeat), eigene Kunden-Verwaltungsseite.
- Lizenz-Ausstellung (Typ/Module/Laufzeit -> signierte Datei via
licensing.issue_license), Detailseite, Download, Widerruf.
- /api/activate, /api/deactivate, /api/heartbeat (unauthentifiziert per
Design -- die Signatur der Anfrage IST der Berechtigungsnachweis) sowie
eine manuelle Offline-Code-Seite, die dieselben drei Verarbeitungs-
funktionen (_process_activate/_process_deactivate/_process_heartbeat)
nutzt wie die Online-API -- ein Protokoll, zwei Transportwege.
- E-Mail-Versand ausgestellter Lizenzen per Microsoft Graph
(Client-Credentials-Flow, reine Standardbibliothek/urllib, keine neue
Abhaengigkeit) inkl. Einrichtungsanleitung und Verbindungstest.
- create_master_license.py: lokales Bootstrap-/Erneuerungs-Skript fuer
die eigene Lizenz des Masters (kein externer Super-Master noetig).
Verifiziert auf dem Testsystem (192.168.82.51): App laeuft parallel zu der
dort laufenden echten TESM-Instanz (Port 5001 vs. 80/5000, eigene
Log-/DB-Pfade, TESM unangetastet). Kompletter ECHTER End-to-End-Rundlauf
per Playwright durchgespielt -- kein simulierter Gegenpart: Kunde anlegen
-> Custom-Lizenz (dhcp+fileshare) ausstellen -> herunterladen -> auf der
echten TESM-Instanz hochladen -> ECHTE Online-Aktivierung (Master
verzeichnet Fingerprint, TESM zeigt 'Aktiviert') -> ECHTE Online-
Deaktivierung (TESM zeigt 'lizenzlos, Export bleibt moeglich', Master
zeigt Status 'Deaktiviert'). Lokale Syntax-/Templatepruefung (py_compile,
pyflakes, jinja2-Parse aller Templates) sauber.
Bewusst NICHT nach main gemergt/getaggt/ausgerollt -- folgt zusammen mit
Phase 3 (Infrastruktur-Umzug + POETEST-Enterprise-Lizenz), siehe Plan
toasty-twirling-hickey.md (Phase 2 von 3).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
3.1 KiB
HTML
68 lines
3.1 KiB
HTML
{#
|
|
Gemeinsame Auditlog-Zeilendarstellung -- von activity_log.html (Erstladung)
|
|
UND _audit_log_rows.html (AJAX-Nachladen über "Weitere 500 laden")
|
|
importiert, damit beide garantiert dieselbe Darstellung erzeugen und
|
|
action_labels/action_icons/category_of nicht an zwei Stellen gepflegt
|
|
werden müssen.
|
|
#}
|
|
{% set action_labels = {
|
|
"settings.update": "Einstellung geändert",
|
|
"device.create": "Gerät angelegt",
|
|
"device.edit": "Gerät bearbeitet",
|
|
"device.delete": "Gerät gelöscht",
|
|
"device.activate": "Gerät aktiviert",
|
|
"device.deactivate": "Gerät deaktiviert",
|
|
"switch.create": "Switch angelegt",
|
|
"switch.edit": "Switch bearbeitet",
|
|
"switch.delete": "Switch gelöscht",
|
|
"credential.create": "Zugangsdaten angelegt",
|
|
"credential.edit": "Zugangsdaten bearbeitet",
|
|
"credential.delete": "Zugangsdaten gelöscht",
|
|
"user.create": "Benutzer angelegt",
|
|
"user.edit": "Benutzer bearbeitet",
|
|
"user.delete": "Benutzer gelöscht",
|
|
"user.assign_group": "Gruppe zugewiesen",
|
|
"group.create": "Gruppe angelegt",
|
|
"group.edit": "Gruppe bearbeitet",
|
|
"group.delete": "Gruppe gelöscht",
|
|
"group.assign_admins": "Admin-Zuweisung geändert",
|
|
"profile.update": "Profil aktualisiert",
|
|
"profile.password": "Passwort geändert",
|
|
"check.run_now": "Prüfung manuell gestartet",
|
|
"fileshare.upload": "Datei(en) hochgeladen",
|
|
"fileshare.mkdir": "Ordner angelegt",
|
|
"fileshare.delete": "Datei/Ordner gelöscht",
|
|
"fileshare.rename": "Datei/Ordner umbenannt",
|
|
"fileshare.download": "Als ZIP heruntergeladen",
|
|
"auditlog.archive": "Auditlog archiviert",
|
|
"auditlog.export_delete": "Auditlog-Archiv exportiert",
|
|
} %}
|
|
{% set action_icons = {
|
|
"delete": '<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/>',
|
|
"create": '<path d="M12 5v14M5 12h14"/>',
|
|
"edit": '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/>',
|
|
"activate": '<path d="M20 6L9 17l-5-5"/>',
|
|
"deactivate": '<circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/>',
|
|
} %}
|
|
{% set create_kinds = ("create", "upload", "mkdir") %}
|
|
|
|
{% macro category_of(kind) %}{% if kind in create_kinds %}create{% elif kind == "delete" %}delete{% else %}edit{% endif %}{% endmacro %}
|
|
|
|
{% macro audit_row(e) %}
|
|
{% set kind = e['action'].split('.')[-1] %}
|
|
{% set cat = category_of(kind)|trim %}
|
|
<tr data-category="{{ cat }}" data-sort-ts="{{ e['ts'] }}" data-sort-user="{{ e['username']|lower }}"
|
|
data-sort-action="{{ action_labels.get(e['action'], e['action'])|lower }}" data-sort-target="{{ (e['target'] or '')|lower }}">
|
|
<td class="text-dim mono" style="font-size:12.5px;">{{ e['ts'] }}</td>
|
|
<td class="cell-name">{{ e['username'] }}</td>
|
|
<td>
|
|
<span class="pill action-pill action-pill--{{ cat }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ (action_icons.get(kind) or action_icons['edit'])|safe }}</svg>
|
|
{{ action_labels.get(e['action'], e['action']) }}
|
|
</span>
|
|
</td>
|
|
<td>{{ e['target'] or '—' }}</td>
|
|
<td class="text-dim">{{ e['details'] or '—' }}</td>
|
|
</tr>
|
|
{% endmacro %}
|