96 lines
9.4 KiB
HTML
96 lines
9.4 KiB
HTML
<!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 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>
|