97 lines
11 KiB
HTML
97 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>{{.PortalTitle}} · {{.Brand}}</title>
|
|
<link rel="stylesheet" href="/assets/app.css">
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span class="brand-mark small">L</span><span>{{.Brand}}</span></a>
|
|
<div class="top-actions"><span class="role-pill">{{.RoleLabel}}</span><span class="user-name">{{.User.DisplayName}}</span><form method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="button ghost" type="submit">Abmelden</button></form></div>
|
|
</header>
|
|
<main class="shell">
|
|
<section class="hero">
|
|
<div><p class="eyebrow">{{.PortalTitle}}</p><h1>{{.Headline}}</h1><p>{{.Subline}}</p></div>
|
|
<div class="status-card"><span class="status-dot {{if .KeysReady}}ok{{else}}warn{{end}}"></span><div><strong>{{if .KeysReady}}Signaturdienst bereit{{else}}Initialisierung erforderlich{{end}}</strong><small>{{if .KeysReady}}Private Keys verschlüsselt gespeichert{{else}}Schlüsselpaare noch nicht erzeugt{{end}}</small></div></div>
|
|
</section>
|
|
{{if .Message}}<div class="alert alert-success">{{.Message}}</div>{{end}}
|
|
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
|
|
|
|
<section class="metrics">
|
|
<article><span>Aktive Lizenzen</span><strong>{{.ActiveCount}}</strong><small>im sichtbaren Mandantenbereich</small></article>
|
|
<article><span>Auslaufend</span><strong>{{.ExpiringCount}}</strong><small>innerhalb von 30 Tagen</small></article>
|
|
<article><span>Widerrufen</span><strong>{{.RevokedCount}}</strong><small>sofort serverseitig gesperrt</small></article>
|
|
<article><span>Validierungs-API</span><strong>Online</strong><small>/api/v1/licenses/validate</small></article>
|
|
</section>
|
|
|
|
{{if eq .User.Role "admin"}}
|
|
<section class="grid two">
|
|
<article class="panel accent-panel">
|
|
<div class="panel-head"><div><p class="eyebrow">Root of trust</p><h2>Schlüsselverwaltung</h2></div><span class="badge">Write-once</span></div>
|
|
{{if .KeysReady}}
|
|
<p class="muted">Die Schlüsselpaare sind erzeugt. Eine erneute Generierung ist in Oberfläche und Backend gesperrt.</p>
|
|
<dl class="key-list"><div><dt>Issuer Key ID</dt><dd>{{.Keys.IssuerKeyID}}</dd></div><div><dt>Lease Key ID</dt><dd>{{.Keys.LeaseKeyID}}</dd></div><div><dt>Erstellt</dt><dd>{{formatTime .Keys.CreatedAt}}</dd></div></dl>
|
|
<label>Öffentlicher Trust Store<textarea readonly rows="7">{{.TrustStoreJSON}}</textarea></label>
|
|
{{else}}
|
|
<p>Erzeuge genau einmal die langfristige Issuer- und die kurzlebige Lease-Autorität. Private Schlüssel werden vor dem Speichern mit dem Master-Key verschlüsselt.</p>
|
|
<form method="post" action="/admin/keys/generate" class="stack compact"><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Issuer Key ID<input name="issuerKeyId" value="issuer-2026" required></label><label>Lease Key ID<input name="leaseKeyId" value="lease-2026" required></label><button class="button primary" type="submit">Schlüsselpaare einmalig erzeugen</button></form>
|
|
<details class="key-import"><summary>Bestehende Schlüsselpaare sicher importieren</summary><form method="post" action="/admin/keys/import" class="stack compact"><input type="hidden" name="csrf" value="{{.CSRF}}"><div class="form-grid"><label>Issuer Key ID<input name="issuerKeyId" required></label><label>Lease Key ID<input name="leaseKeyId" required></label></div><label>Issuer Private Key<input type="password" name="issuerPrivate" autocomplete="off" required></label><label>Lease Private Key<input type="password" name="leasePrivate" autocomplete="off" required></label><button class="button ghost" type="submit">Einmalig importieren und sperren</button></form></details>
|
|
{{end}}
|
|
</article>
|
|
<article class="panel">
|
|
<div class="panel-head"><div><p class="eyebrow">Identity & access</p><h2>Benutzer anlegen</h2></div></div>
|
|
<form method="post" action="/admin/users" class="stack compact"><input type="hidden" name="csrf" value="{{.CSRF}}"><div class="form-grid"><label>Benutzername<input name="username" required></label><label>Anzeigename<input name="displayName" required></label></div><div class="form-grid"><label>Rolle<select name="role"><option value="reseller">Reseller / Autor</option><option value="customer">Kunde / Client</option><option value="admin">Admin / Management</option></select></label><label>Initialpasswort<input type="password" name="password" minlength="12" required></label></div><label>Reseller-Zuordnung (nur für Kunden)<select name="resellerUserId"><option value="">Direktkunde / keine Zuordnung</option>{{range .Resellers}}<option value="{{.ID}}">{{.DisplayName}} (@{{.Username}})</option>{{end}}</select></label><button class="button secondary" type="submit">Account erstellen</button></form>
|
|
<div class="user-list">{{range .Users}}<div><span class="avatar">{{initial .DisplayName}}</span><div><strong>{{.DisplayName}}</strong><small>@{{.Username}} · {{.Role}}</small></div></div>{{end}}</div>
|
|
</article>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{if eq .User.Role "reseller"}}
|
|
<section class="panel">
|
|
<div class="panel-head"><div><p class="eyebrow">Customer onboarding</p><h2>Kundenportal anlegen</h2></div><span class="badge">Mandantentrennung</span></div>
|
|
<p class="muted">Der neue Kunde wird deinem Reseller-Account fest zugeordnet und ist für andere Reseller nicht sichtbar.</p>
|
|
<form method="post" action="/reseller/customers" class="form-grid three"><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Benutzername<input name="username" required></label><label>Anzeigename<input name="displayName" required></label><label>Initialpasswort<input type="password" name="password" minlength="12" required></label><button class="button secondary" type="submit">Kundenportal erstellen</button></form>
|
|
</section>
|
|
{{end}}
|
|
|
|
<section class="panel account-panel">
|
|
<div class="panel-head"><div><p class="eyebrow">Account security</p><h2>Passwort ändern</h2></div><span class="badge">Session reset</span></div>
|
|
<form method="post" action="/account/password" class="form-grid three"><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Aktuelles Passwort<input type="password" name="currentPassword" autocomplete="current-password" required></label><label>Neues Passwort<input type="password" name="newPassword" minlength="12" autocomplete="new-password" required></label><label> <button class="button ghost" type="submit">Passwort aktualisieren</button></label></form>
|
|
</section>
|
|
|
|
{{if or (eq .User.Role "admin") (eq .User.Role "reseller")}}
|
|
<section class="panel">
|
|
<div class="panel-head"><div><p class="eyebrow">License composer</p><h2>Neue Lizenz ausstellen</h2></div><span class="badge badge-live">signiert</span></div>
|
|
{{if .KeysReady}}
|
|
<form method="post" action="/licenses" class="stack"><input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<div class="form-grid three"><label>Kunden-Account<select name="customerUserId"><option value="">Ohne Portal-Zuordnung</option>{{range .Customers}}<option value="{{.ID}}">{{.DisplayName}} (@{{.Username}})</option>{{end}}</select></label><label>Kundenname<input name="customer" required></label><label>Produkt-ID<input name="product" placeholder="my-product" required></label></div>
|
|
<div class="form-grid three"><label>Edition<input name="edition" placeholder="professional" required></label><label>Modus<select name="mode"><option value="hybrid">Hybrid offline</option><option value="online">Online erforderlich</option><option value="offline">Vollständig offline</option></select></label><label>Gültigkeit (Tage)<input type="number" name="days" min="1" max="3650" value="365" required></label></div>
|
|
<div class="form-grid"><label>Features<textarea name="features" placeholder="advanced_export, white_label"></textarea></label><label>Limits<textarea name="limits" placeholder="users=50 sites=10"></textarea></label></div>
|
|
<div class="form-grid"><label>Domains<input name="domains" value="*" placeholder="example.org, *.example.org"></label><label>Instanz-IDs<input name="instances" placeholder="optional"></label></div>
|
|
<div class="form-grid three"><label>Lease TTL (Minuten)<input type="number" name="leaseMinutes" min="1" max="1440" value="60"></label><label>Offline Grace (Stunden)<input type="number" name="graceHours" min="0" max="8760" value="72"></label><label>Server-URL<input value="{{.PublicURL}}" readonly></label></div>
|
|
<button class="button primary" type="submit">Lizenz signieren und registrieren</button>
|
|
</form>
|
|
{{else}}<div class="empty">Ein Administrator muss zuerst die Schlüsselpaare initialisieren.</div>{{end}}
|
|
</section>
|
|
{{end}}
|
|
|
|
<section class="panel">
|
|
<div class="panel-head"><div><p class="eyebrow">Portfolio</p><h2>{{if eq .User.Role "customer"}}Meine Lizenzen{{else}}Lizenzbestand{{end}}</h2></div><span class="badge">{{len .Licenses}} Einträge</span></div>
|
|
<div class="table-wrap"><table><thead><tr><th>Lizenz</th><th>Kunde / Produkt</th><th>Modus</th><th>Status</th><th>Gültig bis</th><th>Aktion</th></tr></thead><tbody>
|
|
{{range .Licenses}}<tr><td><strong>{{.LicenseID}}</strong><small>{{.Edition}}</small></td><td>{{.Customer}}<small>{{.Product}}</small></td><td><span class="mode">{{.Mode}}</span></td><td>{{if .Revoked}}<span class="status revoked">Widerrufen</span>{{else}}<span class="status active">Aktiv</span>{{end}}</td><td>{{formatTime .ExpiresAt}}</td><td><div class="row-actions"><a class="button tiny ghost" href="/licenses/{{.LicenseID}}/token">Token</a>{{if or (eq $.User.Role "admin") (and (eq $.User.Role "reseller") (eq .IssuedByUserID $.User.ID))}}<form method="post" action="/licenses/{{.LicenseID}}/{{if .Revoked}}restore{{else}}revoke{{end}}"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button class="button tiny {{if .Revoked}}secondary{{else}}danger{{end}}" type="submit">{{if .Revoked}}Aktivieren{{else}}Sperren{{end}}</button></form>{{end}}</div></td></tr>{{else}}<tr><td colspan="6"><div class="empty">Noch keine Lizenzen vorhanden.</div></td></tr>{{end}}
|
|
</tbody></table></div>
|
|
</section>
|
|
|
|
{{if eq .User.Role "admin"}}
|
|
<section class="panel">
|
|
<div class="panel-head"><div><p class="eyebrow">Audit trail</p><h2>Letzte Sicherheitsereignisse</h2></div></div>
|
|
<div class="audit-list">{{range .Audit}}<div><time>{{formatTime .CreatedAt}}</time><strong>{{.Action}}</strong><span>{{.Target}}</span><small>{{.Detail}}</small></div>{{else}}<div class="empty">Noch keine Ereignisse.</div>{{end}}</div>
|
|
</section>
|
|
{{end}}
|
|
</main>
|
|
<footer><span>{{.Brand}}</span><span>Ed25519 · AES-256-GCM · CSRF · Audit</span></footer>
|
|
</body>
|
|
</html>
|