go-loose/web/index.html
2026-10-04 12:24:13 +02:00

96 lines
9.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" data-theme="tokyo" data-mode="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark light">
<title>Go Loose · API access control</title>
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<div class="noise"></div>
<header>
<a class="brand" href="/"><span class="brand-mark">GL</span><span>Go Loose</span></a>
<nav>
<a href="/docs">API docs</a>
<label class="compact">Tenant<select id="tenant-context"></select></label>
<label class="compact">Theme
<select id="theme"><option value="tokyo">Tokyo Night</option><option value="catppuccin">Catppuccin</option><option value="tomorrow">Tomorrow</option><option value="kanagawa">Kanagawa</option></select>
</label>
<button id="mode" class="icon-button" title="Toggle light/dark mode">◐</button>
<a id="identity" class="identity" href="/profile"><img id="identity-avatar" alt="" width="22" height="22" hidden><span id="identity-name"></span></a>
<form action="/auth/logout" method="post"><button class="ghost" type="submit">Log out</button></form>
</nav>
</header>
<main>
<section class="hero">
<div>
<p class="eyebrow">MULTI-TENANT API ACCESS CONTROL</p>
<h1>One key plane.<br><span>Every Go API.</span></h1>
<p class="lede">Register applications, issue scoped keys, import OpenAPI contracts, and authorize requests without coupling identity logic to every service.</p>
</div>
<div class="status-card">
<div class="pulse"></div>
<div><strong>Authorization plane</strong><span>Operational</span></div>
<code>POST /api/v1/authorize</code>
</div>
</section>
<div id="notice" class="notice" hidden></div>
<section class="stats" id="stats">
<article><span>Tenants</span><strong id="tenant-count">—</strong></article>
<article><span>Applications</span><strong id="app-count">—</strong></article>
<article><span>API keys</span><strong id="key-count">—</strong></article>
<article><span>Contracts</span><strong id="contract-count">—</strong></article>
<article><span>Users</span><strong id="user-count">—</strong></article>
</section>
<section class="workspace">
<aside>
<button class="tab active" data-panel="applications">01 <span>Applications</span></button>
<button class="tab" data-panel="keys">02 <span>API keys</span></button>
<button class="tab" data-panel="contracts">03 <span>Contracts</span></button>
<button class="tab" data-panel="tenants">04 <span>Tenants</span></button>
<button class="tab" data-panel="users">05 <span>Users</span></button>
</aside>
<div class="panels">
<section class="panel active" id="applications">
<div class="panel-head"><div><p class="eyebrow">SERVICE CATALOG</p><h2>Applications</h2></div><button data-dialog="app-dialog" data-tenant-application-action>New application</button></div>
<div class="card-grid" id="app-list"></div>
</section>
<section class="panel" id="keys">
<div class="panel-head"><div><p class="eyebrow">CREDENTIALS</p><h2>API keys</h2></div><button data-dialog="key-dialog" data-tenant-key-action>Issue key</button></div>
<div class="table-wrap"><table><thead><tr><th>Name</th><th>Application</th><th>Prefix</th><th>Status</th><th>Last used</th><th></th></tr></thead><tbody id="key-list"></tbody></table></div>
</section>
<section class="panel" id="contracts">
<div class="panel-head"><div><p class="eyebrow">DISCOVERY</p><h2>OpenAPI contracts</h2></div><button data-dialog="contract-dialog" data-tenant-contract-action>Import contract</button></div>
<div class="table-wrap"><table><thead><tr><th>Application</th><th>OpenAPI</th><th>Endpoints</th><th>Source</th><th>Imported</th></tr></thead><tbody id="contract-list"></tbody></table></div>
</section>
<section class="panel" id="tenants">
<div class="panel-head"><div><p class="eyebrow">ORGANIZATIONS</p><h2>Tenants</h2></div><button id="new-tenant" data-dialog="tenant-dialog">New tenant</button></div>
<div class="card-grid" id="tenant-list"></div>
</section>
<section class="panel" id="users">
<div class="panel-head"><div><p class="eyebrow">CLIENT IDENTITIES</p><h2>Users & access</h2></div><button data-dialog="user-dialog" data-tenant-user-action>Invite user</button></div>
<div class="table-wrap"><table><thead><tr><th>User</th><th>Tenant</th><th>Role</th><th>Status</th><th>Applications</th><th></th></tr></thead><tbody id="user-list"></tbody></table></div>
<p class="muted">Roles come from the shared permission matrix. <a href="/profile">Open your own profile</a> to change your name and picture.</p>
</section>
</div>
</section>
</main>
<dialog id="tenant-dialog"><form id="tenant-form"><div class="dialog-head"><h3>Create tenant</h3><button type="button" class="close">×</button></div><label>Name<input name="name" required></label><label>Slug<input name="slug" required pattern="[a-z0-9][a-z0-9-]{1,62}" placeholder="nmbs"></label><button type="submit">Create tenant</button></form></dialog>
<dialog id="app-dialog"><form id="app-form"><div class="dialog-head"><h3>Register application</h3><button type="button" class="close">×</button></div><label>Tenant<select name="tenant_id" class="tenant-select" required></select></label><label>Name<input name="name" required></label><label>Slug<input name="slug" required pattern="[a-z0-9][a-z0-9-]{1,62}" placeholder="guess"></label><label>Description<textarea name="description"></textarea></label><label>Allowed hosts <small>(one per line, empty allows all)</small><textarea name="allowed_hosts" placeholder="nmbs.guess.local"></textarea></label><button type="submit">Register application</button></form></dialog>
<dialog id="key-dialog"><form id="key-form"><div class="dialog-head"><h3>Issue API key</h3><button type="button" class="close">×</button></div><label>Application<select name="application_id" class="app-select" required></select></label><label>Key name<input name="name" required placeholder="production-gateway"></label><label>Expires at <small>(optional)</small><input type="datetime-local" name="expires_at"></label><button type="submit">Issue key</button></form></dialog>
<dialog id="contract-dialog"><form id="contract-form"><div class="dialog-head"><h3>Import OpenAPI contract</h3><button type="button" class="close">×</button></div><label>Application<select name="application_id" class="app-select" required></select></label><div class="segmented"><label><input type="radio" name="source_type" value="document" checked> Paste</label><label><input type="radio" name="source_type" value="url"> Scan URL</label></div><label>Source URL <small>(optional when pasting)</small><input type="url" name="source_url" placeholder="https://service/openapi.json"></label><label>OpenAPI JSON or YAML<textarea name="document" rows="12" placeholder="openapi: 3.1.0&#10;paths: ..."></textarea></label><button type="submit">Import contract</button></form></dialog>
<dialog id="client-dialog"><form id="client-form"><div class="dialog-head"><h3>Configure client login</h3><button type="button" class="close">×</button></div><input type="hidden" name="application_id"><label>Client ID<input name="client_id" readonly></label><label>Redirect URIs <small>(one exact callback URL per line)</small><textarea name="redirect_uris" rows="5" placeholder="https://nmbs.guess.local/auth/callback" required></textarea></label><p class="muted">Saving rotates the client secret. Existing deployments must be updated immediately.</p><button type="submit">Save and rotate secret</button></form></dialog>
<dialog id="user-dialog"><form id="user-form"><div class="dialog-head"><h3>Invite tenant user</h3><button type="button" class="close">×</button></div><label>Tenant<select name="tenant_id" class="tenant-select" required></select></label><label>Email<input type="email" name="email" required></label><label>Display name<input name="display_name"></label><label>Initial password <small>(minimum 12 characters)</small><input type="password" name="password" minlength="12" autocomplete="new-password" required></label><label>Role<select name="role" id="invite-role" required></select></label><button type="submit">Invite user</button></form></dialog>
<dialog id="access-dialog"><form id="access-form"><div class="dialog-head"><h3>Manage user access</h3><button type="button" class="close">×</button></div><input type="hidden" name="user_id"><input type="hidden" name="tenant_id"><input type="hidden" name="approval_mode"><label id="approval-tenant-label" hidden>Tenant<select name="approval_tenant_id" class="tenant-select"></select></label><label>Role<select name="role" id="access-role" required></select></label><fieldset><legend>Applications</legend><div id="access-apps" class="check-list"></div></fieldset><button type="submit">Update access</button></form></dialog>
<dialog id="secret-dialog"><div class="dialog-head"><h3 id="secret-title">Secret issued</h3><button type="button" class="close">×</button></div><p id="secret-description">This secret is shown once. Copy it now.</p><div class="secret"><code id="new-secret"></code><button id="copy-secret" type="button">Copy</button></div></dialog>
<script src="/assets/app.js" defer></script>
</body>
</html>