Master-Lizenzserver Phase 2: neue Schwester-Anwendung srv/tesm-license (v1.0.0)
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>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "index" %}
|
||||
{% block page_title %}Lizenz ausstellen{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">Neue signierte Lizenz für einen Kunden erzeugen</div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% import "_hint_icon.html" as hi %}
|
||||
<div class="settings-grid">
|
||||
|
||||
<div class="card card-pad">
|
||||
{% if not master_licensed %}
|
||||
<div class="notice-banner notice-banner--warning" style="margin-bottom:14px;">
|
||||
Dieser Lizenzserver hat selbst keine gültige Lizenz — siehe <a href="{{ url_for('settings_license') }}">Lizenz</a>.
|
||||
Ausstellen ist erst danach wieder möglich.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
<div class="field">
|
||||
<label for="customer_id">Kunde</label>
|
||||
<select name="customer_id" id="customer_id" onchange="toggleNewCustomerFields()" required>
|
||||
<option value="__new__" {% if not preselect_customer_id %}selected{% endif %}>+ Neuer Kunde…</option>
|
||||
{% for c in customers %}
|
||||
<option value="{{ c.id }}" {% if preselect_customer_id == c.id %}selected{% endif %}>{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="newCustomerFields" class="field-group">
|
||||
<div class="field">
|
||||
<label for="new_customer_name">Name (neuer Kunde)</label>
|
||||
<input type="text" name="new_customer_name" id="new_customer_name" placeholder="z.B. Musterfirma GmbH">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="new_customer_email">E-Mail (neuer Kunde)</label>
|
||||
<input type="email" name="new_customer_email" id="new_customer_email">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="license_type">Lizenztyp {{ hi.hint_icon("Trial: zeitlich begrenzt, voller Zugriff. Standard: alle lizenzpflichtigen Basis-Funktionen, keine Zusatzmodule. Custom: Standard + einzeln wählbare Module. Enterprise: alles.", "Lizenztyp") }}</label>
|
||||
<select name="license_type" id="license_type" onchange="toggleModuleFields()" required>
|
||||
{% for t in license_types %}
|
||||
<option value="{{ t }}" {% if t == 'standard' %}selected{% endif %}>{{ type_labels.get(t, t) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="moduleFields" class="field hidden">
|
||||
<label>Module</label>
|
||||
<div class="check-list">
|
||||
{% for m in all_modules %}
|
||||
<label class="check-row">
|
||||
<input type="checkbox" name="modules" value="{{ m }}">
|
||||
{{ module_labels.get(m, m) }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="valid_days">Gültigkeitsdauer (Tage) {{ hi.hint_icon("365 = 1 Jahr, 730 = 2 Jahre. Frei wählbar.", "Gültigkeitsdauer") }}</label>
|
||||
<input type="number" name="valid_days" id="valid_days" value="365" min="1" required>
|
||||
<div class="field-hint">
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="document.getElementById('valid_days').value=30">30 Tage (Trial)</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="document.getElementById('valid_days').value=365">1 Jahr</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="document.getElementById('valid_days').value=730">2 Jahre</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block" {% if not master_licensed %}disabled{% endif %}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
Lizenz ausstellen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleNewCustomerFields() {
|
||||
const isNew = document.getElementById("customer_id").value === "__new__";
|
||||
document.getElementById("newCustomerFields").classList.toggle("hidden", !isNew);
|
||||
}
|
||||
function toggleModuleFields() {
|
||||
const type = document.getElementById("license_type").value;
|
||||
document.getElementById("moduleFields").classList.toggle("hidden", type !== "custom");
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
toggleNewCustomerFields();
|
||||
toggleModuleFields();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user