Files
tesm-license/srv/tesm-license/templates/self_service.html
T
alientimandClaude Sonnet 5 2d920f581f Eigenstaendige Lizenzen-Seite, entschachtelte Ticket-Ansicht, zeitlich begrenzter Kundenportal-Zugang (v1.2.2)
Reaktion auf Feedback: 'Tickets und Lizenzen je Kunden war sehr sehr
unintuitiv dargestellt... Kunde -> Ticket -> Lizenz aber alles auf extra
Seiten, im Menü einzeln anwählbar ohne diese riesen Verschachtelung.'

## Lizenzen als eigene, gleichberechtigte Ebene (neu: /licenses)

Bisher war eine Lizenz nur ueber Kunde -> gefilterte Tickets -> Ticket-
Detail -> eingebettete Historie-Tabelle erreichbar. Jetzt: Kunden,
Tickets UND Lizenzen sind drei eigene, direkt in der Navigation
erreichbare Seiten (Reihenfolge im Menü spiegelt bewusst die fachliche
Hierarchie). Kunden- und Ticket-Seite verlinken mit Filter (?customer_id=/
?ticket_id=) dorthin, statt Lizenzdaten selbst einzubetten.

Ticket-Detailseite dadurch massiv entschlackt: statt einer vollen
Lizenz-Historie-Tabelle nur noch eine kompakte 'Aktuelle Lizenz'-Zeile
mit Link auf die (ggf. gefilterte) Lizenzen-Seite fuer die volle Historie.

## Kundenportal: kein dauerhafter Link mehr, sondern zeitlich begrenzter Zugang

Vorheriger Ansatz (v1.2.0): ein dauerhafter, unbefristeter Token pro
Kunde, von Admins verteilt. Auf Nachfrage ('wie bekommt der Kunde seinen
Magic-Link, wenn er ihn verloren hat?') kam eine oeffentliche
'Zugang anfordern'-Seite dazu (E-Mail eingeben -> Link zugeschickt) --
und direkt im Anschluss die berechtigte Sicherheitsfrage, ob so ein
oeffentlich auf blossen Zuruf ausloesbarer Link nicht zeitlich begrenzt
sein sollte. Ergebnis: der dauerhafte Link wurde komplett entfernt.

Neuer, einziger Weg ins Kundenportal:
1. /portal/request -- Kunde gibt seine E-Mail ein (oeffentlich, keine
   Anmeldung noetig). Immer dieselbe generische Erfolgsmeldung,
   unabhaengig davon ob die Adresse bekannt ist (keine Enumeration).
2. Bei bekannter Adresse: ein frischer, einmalig nutzbarer Zugangs-Token
   (portal_access_tokens-Tabelle) wird per E-Mail verschickt, gueltig nur
   PORTAL_ACCESS_LINK_MINUTES (30) ab Anforderung.
3. /portal/access/<token> prueft Gueltigkeit + Einmalnutzung, markiert
   den Token als benutzt und setzt eine Session, die ihrerseits nach
   PORTAL_SESSION_MINUTES (60) automatisch ablaeuft.
4. /portal/me zeigt das eigentliche Portal (alle Tickets/Lizenzen,
   Kontaktdaten pflegen) -- ausschliesslich session-basiert erreichbar.

license_customers.portal_token bleibt in der DB bestehen (keine riskante
Schema-Ruecknahme), wird fuer die Zugriffsentscheidung aber nicht mehr
herangezogen -- alle Stellen, die vorher den dauerhaften Portal-Link
eingebettet haben (Ticket-Seite, Lizenz-per-E-Mail, Self-Service-Seite),
verweisen jetzt stattdessen auf /portal/request.

Verifiziert: bestehende Funktionstests erneut komplett durchlaufen (neue
Lizenzen-Seite, entschachtelte Ticket-Seite, entfernter dauerhafter
Zugang liefert 404), plus 11 neue Tests fuer den kompletten Magic-Link-
Fluss (Token-Erzeugung, Einmalnutzung, Ablehnen bei Wiederverwendung/
unbekanntem/abgelaufenem Token, Session bleibt nach Erstnutzung gueltig,
/portal/me ohne Session leitet zur Anfrage-Seite um) -- alle bestanden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 17:03:55 +02:00

87 lines
4.1 KiB
HTML

<!DOCTYPE html>
<html lang="de" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Meine Lizenz · TESM-Lizenzserver</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/icon-dark.svg') }}" id="app-favicon">
<link rel="stylesheet" href="{{ asset_url('css/style.css') }}">
</head>
<body>
<div class="login-page">
<div class="login-bg">
<img src="{{ url_for('static', filename='images/logo-dark.svg') }}" alt="TESM-Lizenzserver" id="login-bg-logo">
</div>
<div class="login-card" style="max-width:520px;">
<h1>Meine Lizenz</h1>
<div class="hint" style="text-align:center; margin-bottom:18px;">
{{ customer.name if customer else '' }}
<br><a href="{{ portal_request_url }}">← Alle meine Tickets anzeigen</a>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="field" style="margin-bottom:18px;">
{% for message in messages %}
<div class="toast danger" style="position:static; animation:none;">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% if ticket.status != 'open' %}
<div class="notice-banner notice-banner--warning" style="margin-bottom:14px;">
Dieses Ticket ist gesperrt. Bitte wenden Sie sich an Ihren Ansprechpartner.
</div>
{% endif %}
<div class="field"><label>Lizenztyp</label><input type="text" value="{{ type_labels.get(ticket.type, ticket.type) }}" disabled></div>
<div class="field"><label>Module</label><input type="text" value="{{ modules_list|join(', ') if modules_list else 'keine' }}" disabled></div>
{% if license %}
<div class="field">
<label>Status</label>
<input type="text" value="{{ {'issued': 'Ausgestellt (noch nicht aktiviert)', 'active': 'Aktiv', 'deactivated': 'Deaktiviert', 'revoked': 'Widerrufen'}.get(license.status, license.status) }}" disabled>
</div>
<div class="field"><label>Gültig bis</label><input type="text" value="{{ 'Lifetime (läuft nie ab)' if license.is_lifetime else license.expires_at }}" disabled></div>
{% if license.status in ('issued', 'active') %}
<a href="{{ url_for('self_service_download', ticket_id=ticket.id) }}" class="btn btn-secondary btn-block" style="margin-bottom:10px;">
Lizenzdatei herunterladen
</a>
<form method="post" action="{{ url_for('self_service_revoke', ticket_id=ticket.id) }}"
data-confirm="Lizenz wirklich widerrufen? Das aktuelle System verliert danach den Zugriff. Sie können anschließend jederzeit eine neue Lizenz erstellen.">
<button type="submit" class="btn btn-danger btn-block">Lizenz widerrufen (z.B. bei Systemwechsel)</button>
</form>
{% else %}
<a href="{{ url_for('self_service_download', ticket_id=ticket.id) }}" class="btn btn-secondary btn-block">
Letzte Lizenzdatei herunterladen
</a>
{% endif %}
{% else %}
<p class="text-faint" style="font-size:12.5px;">Für dieses Ticket wurde noch keine Lizenz erstellt.</p>
{% endif %}
{% if can_issue_new %}
<hr style="border:none; border-top:1px solid var(--border-soft); margin:18px 0;">
<form method="post" action="{{ url_for('self_service_issue', ticket_id=ticket.id) }}" style="margin-bottom:10px;">
<input type="hidden" name="issue_license" value="regular">
<button type="submit" class="btn btn-primary btn-block">
Neue {{ type_labels.get(ticket.type, ticket.type) }}-Lizenz erstellen ({{ 'Lifetime' if ticket.lifetime else ticket.valid_days ~ ' Tage' }})
</button>
</form>
<form method="post" action="{{ url_for('self_service_issue', ticket_id=ticket.id) }}">
<input type="hidden" name="issue_license" value="trial">
<button type="submit" class="btn btn-secondary btn-block">Stattdessen Trial erstellen (30 Tage, alle Module)</button>
</form>
{% endif %}
<div style="margin-top:16px; font-size:10.5px; color:var(--text-faint); text-align:center;">TESM-Lizenzserver</div>
</div>
</div>
<script src="{{ asset_url('js/app.js') }}"></script>
</body>
</html>