init
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package webassets
|
||||
|
||||
import "embed"
|
||||
|
||||
// FS contains the portal templates and static assets.
|
||||
//
|
||||
//go:embed templates/*.html static/*
|
||||
var FS embed.FS
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Login · {{.Brand}}</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<main class="auth-shell">
|
||||
<section class="auth-brand">
|
||||
<div class="brand-mark" aria-hidden="true">L</div>
|
||||
<p class="eyebrow">License infrastructure</p>
|
||||
<h1>Eine zentrale Autorität für jede Produktlizenz.</h1>
|
||||
<p>Offline signierte Lizenzen, kurzlebige Online-Leases und getrennte Portale für Management, Reseller und Kunden.</p>
|
||||
<div class="trust-row"><span>Ed25519</span><span>AES-256-GCM</span><span>Hybrid offline</span></div>
|
||||
</section>
|
||||
<section class="auth-card">
|
||||
<div>
|
||||
<p class="eyebrow">Geschützter Bereich</p>
|
||||
<h2>Anmelden</h2>
|
||||
<p class="muted">Verwende deinen persönlichen Account. Zugriffe werden protokolliert.</p>
|
||||
</div>
|
||||
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
|
||||
<form method="post" action="/login" class="stack">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<label>Benutzername<input name="username" autocomplete="username" required autofocus></label>
|
||||
<label>Passwort<input type="password" name="password" autocomplete="current-password" required></label>
|
||||
<button class="button primary" type="submit">Sicher anmelden</button>
|
||||
</form>
|
||||
<p class="micro">Sessions sind HttpOnly, SameSite=Strict und zeitlich begrenzt.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lizenz-Token · {{.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><a class="button ghost" href="/">Zurück</a></header><main class="shell narrow"><section class="hero"><div><p class="eyebrow">Credential delivery</p><h1>{{.License.LicenseID}}</h1><p>Dieser Token enthält keine privaten Schlüssel. Er kann als Secret im Zielsystem hinterlegt werden.</p></div></section><section class="panel"><div class="panel-head"><div><h2>Client-Konfiguration</h2></div><span class="badge">{{.License.Mode}}</span></div><label>Environment<textarea rows="8" readonly>LICENSE_TOKEN={{.Token}}
|
||||
LICENSE_MODE={{.License.Mode}}
|
||||
LICENSE_SERVER_URL={{.License.ServerURL}}
|
||||
LICENSE_CACHE_FILE=./data/license-lease.json
|
||||
LICENSE_REFRESH_INTERVAL=15m
|
||||
LICENSE_REQUEST_TIMEOUT=5s</textarea></label><label>Signierter Lizenz-Token<textarea rows="12" readonly>{{.Token}}</textarea></label><div class="alert">Die Server-URL ist zusätzlich signiert im Token hinterlegt. Moderne Clients erkennen sie automatisch; ENV überschreibt die automatische Erkennung.</div></section></main></body></html>
|
||||
Reference in New Issue
Block a user