Die Lizenzseite (Einstellungen -> Lizenz) hatte im Rechtebaum keinen eigenen Eintrag -- sie lief komplett ueber settings_system.view/.edit mit, genau wie im TESM-Client (siehe dortiger Fix). Damit konnte 'Lizenz verwalten' nicht unabhaengig von 'Systemeinstellungen verwalten' vergeben/eingeschraenkt werden. - Neues Rechtepaar settings_license.view/settings_license.edit (PERMISSIONS/PERMISSION_LABELS), Property can_view_settings_license. - Nav-Sichtbarkeit und alle /settings/license*-Routen auf das neue Recht umgestellt (view-Route: can_view_settings_license; alle mutierenden Routen -upload/-activate(/confirm)/-deactivate(/confirm): settings_license.edit). Das 'auch ohne aktive Lizenz sichtbar bleiben'-Verhalten bleibt unveraendert (andere Pruefung, license_active()). - settings_license.html: can_edit auf settings_license.edit umgestellt. - Migration (_migrated_settings_license_permission_v1, einmalig geguarded): jede Gruppe mit settings_system.view/.edit bekommt automatisch settings_license.view/.edit dazu -- niemand verliert beim Update stillschweigend Zugriff, den er vorher implizit hatte. - Getestet gegen isolierte Test-DB (create_db.py-Frischinstallation): Migration korrekt (inkl. Gruppe mit nur .view, die korrekt KEIN .edit bekommt), idempotent bei zweitem Lauf, keine Duplikate. PERMISSIONS/ PERMISSION_LABELS/NAV_ITEMS strukturell konsistent geprueft, app.py syntaktisch fehlerfrei kompiliert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
180 lines
8.0 KiB
HTML
180 lines
8.0 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "settings_license" %}
|
|
{% set can_edit = current_user.has_permission('settings_license.edit') %}
|
|
{% block page_title %}Lizenz{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">Lizenzstatus, Module und Aktivierung dieses Systems</div>{% endblock %}
|
|
|
|
{% block content %}
|
|
{% import "_hint_icon.html" as hi %}
|
|
<div class="settings-grid">
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Status</h2>
|
|
<div class="hint">Aktuell installierte Lizenz und ihr Zustand.</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if license_error %}
|
|
<div class="notice-banner notice-banner--warning" style="margin-bottom:14px;">{{ license_error }}</div>
|
|
{% endif %}
|
|
|
|
{% if not license_file %}
|
|
<p class="text-faint" style="font-size:12.5px;">Keine Lizenzdatei vorhanden — bitte unten eine Lizenzdatei hochladen.</p>
|
|
{% else %}
|
|
<div class="field">
|
|
<label>Kunde</label>
|
|
<input type="text" value="{{ license_file.customer.name or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Lizenztyp</label>
|
|
<input type="text" value="{{ {'trial': 'Trial', 'standard': 'Standard', 'custom': 'Custom', 'enterprise': 'Enterprise'}.get(license_file.type, license_file.type) }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Module {{ hi.hint_icon("Zusätzlich zu den immer verfügbaren Standard-Funktionen freigeschaltete Vollmodule.", "Module") }}</label>
|
|
<input type="text" value="{{ (license_file.modules or [])|join(', ') if license_file.modules else 'keine' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Ausgestellt am</label>
|
|
<input type="text" value="{{ license_file.issued_at or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Läuft ab am</label>
|
|
<input type="text" value="{{ license_file.expires_at or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Aktivierungsstatus</label>
|
|
<input type="text" value="{{ 'Aktiviert' if license_is_activated else 'Nicht aktiviert' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Letzter Heartbeat</label>
|
|
<input type="text" value="{{ license_last_heartbeat_at or 'noch nie' }}" disabled>
|
|
</div>
|
|
{% if license_last_error %}
|
|
<div class="field">
|
|
<label>Letzter Fehler</label>
|
|
<input type="text" value="{{ license_last_error }}" disabled>
|
|
</div>
|
|
{% endif %}
|
|
<div class="field">
|
|
<label>System-Fingerabdruck {{ hi.hint_icon("Eindeutiger Identifikator dieses Systems, an den die Lizenz bei der Aktivierung gebunden wird.", "System-Fingerabdruck") }}</label>
|
|
<input type="text" class="mono" value="{{ license_fingerprint }}" disabled>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if license_file and license_valid %}
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Aktivierung</h2>
|
|
<div class="hint">
|
|
Ein Protokoll, zwei Wege: automatisch online, oder — falls kein Netzwerkzugriff auf den
|
|
Lizenzserver besteht — per Code manuell mit dem Lizenzserver-Administrator ausgetauscht.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if not can_edit %}
|
|
<p class="text-faint" style="font-size:12.5px;">Für Aktivierung/Deaktivierung fehlt das Recht „Systemeinstellungen ändern“.</p>
|
|
|
|
{% elif pending_action %}
|
|
<p class="text-faint" style="font-size:12.5px; margin-bottom:10px;">
|
|
Offene {{ 'Aktivierungs' if pending_action.action == 'activate' else 'Deaktivierungs' }}-Anfrage — folgenden Code
|
|
beim Lizenzserver-Administrator eingeben:
|
|
</p>
|
|
<div class="field">
|
|
<textarea class="mono" rows="4" readonly onclick="this.select()" style="width:100%; resize:vertical;">{{ pending_code }}</textarea>
|
|
</div>
|
|
<form method="post" action="{{ url_for('license_activate_confirm' if pending_action.action == 'activate' else 'license_deactivate_confirm') }}">
|
|
<div class="field">
|
|
<label for="confirmation_code">Bestätigungscode vom Lizenzserver</label>
|
|
<textarea class="mono" name="confirmation_code" id="confirmation_code" rows="4" style="width:100%; resize:vertical;" required></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Code bestätigen
|
|
</button>
|
|
</form>
|
|
<form method="post" action="{{ url_for('license_activate' if pending_action.action == 'activate' else 'license_deactivate') }}" style="margin-top:10px;">
|
|
<button type="submit" class="btn btn-secondary btn-block">Erneut online versuchen</button>
|
|
</form>
|
|
|
|
{% elif license_is_activated %}
|
|
<p class="text-faint" style="font-size:12.5px; margin-bottom:10px;">
|
|
Lizenz ist aktiviert. Bei einem Systemwechsel zuerst hier deaktivieren — danach kann der Kunde sich beim
|
|
Anbieter selbst eine neue Lizenz für das neue System ausstellen lassen.
|
|
</p>
|
|
<form method="post" action="{{ url_for('license_deactivate') }}"
|
|
data-confirm="Lizenz wirklich deaktivieren? Alle lizenzpflichtigen Module/Funktionen werden danach sofort inaktiv (Export bleibt bis zur nächsten Aktivierung möglich).">
|
|
<button type="submit" class="btn btn-danger btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>
|
|
Lizenz deaktivieren (Systemwechsel)
|
|
</button>
|
|
</form>
|
|
|
|
{% else %}
|
|
<form method="post" action="{{ url_for('license_activate') }}">
|
|
<button type="submit" class="btn btn-primary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Jetzt aktivieren
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Lizenzdatei hochladen</h2>
|
|
<div class="hint">Vom Anbieter erhaltene Lizenzdatei einspielen — muss danach noch aktiviert werden.</div>
|
|
</div>
|
|
</div>
|
|
{% if can_edit %}
|
|
<form method="post" action="{{ url_for('license_upload') }}" enctype="multipart/form-data">
|
|
<div class="field">
|
|
<label for="license_file">Lizenzdatei</label>
|
|
<input type="file" name="license_file" id="license_file" accept=".json,.lic" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-secondary btn-block">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/></svg>
|
|
Hochladen
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<p class="text-faint" style="font-size:12.5px;">Für den Upload fehlt das Recht „Systemeinstellungen ändern“.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if license_file and license_file.vendor %}
|
|
<div class="card card-pad">
|
|
<div class="section-head" style="margin-bottom:16px;">
|
|
<div>
|
|
<h2 style="font-size:16px;">Anbieter</h2>
|
|
<div class="hint">Kontakt für Rückfragen zu dieser Lizenz.</div>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label>Firma</label>
|
|
<input type="text" value="{{ license_file.vendor.name or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Telefon</label>
|
|
<input type="text" value="{{ license_file.vendor.phone or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>E-Mail</label>
|
|
<input type="text" value="{{ license_file.vendor.email or '' }}" disabled>
|
|
</div>
|
|
<div class="field">
|
|
<label>Anschrift</label>
|
|
<input type="text" value="{{ license_file.vendor.address or '' }}" disabled>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
{% endblock %}
|