go-loose/web/index.html

111 lines
10 KiB
HTML
Raw Normal View History

2026-10-01 23:15:57 +02:00
<!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">
feat: account menu, database roles, and single logout Bundles the console work from the current branch: the header account control, database-backed roles and permissions, and signing out across the applications that share this sign-in. The account control replaces a separate identity chip and a separate sign-out button, which meant the header carried two controls describing one fact. web/account.js is mounted into every page by a placeholder and owns the keyboard handling, the outside-click behaviour and the sign-out form once. It also fills the trigger's name, which the previous markup left empty. Signing out revokes the hosted-login bearer sessions this installation issued, then redirects to the provider's end_session_endpoint when the discovery document advertises one. Applications that authenticate against this installation therefore lose their session too; one pointed straight at an identity provider is out of its reach and is unchanged. Roles and permissions become rows rather than a closed enum, so a tenant can extend the catalog without a migration, and a role can no longer be granted a capability that is not in it. requireSameOrigin now compares against the origin the request arrived on instead of the configured base URL, which defaulted to http://localhost:8080 and so rejected every write in a deployment that never set it. Two bugs found while verifying in a browser: - A profile picture never rendered. avatarURL built an absolute URL from the configured base URL, and the page's own img-src 'self' blocked it. The console's own pages now take a root-relative path; only /connect/userinfo, read from another origin, needs an absolute one, and it follows the request. - absoluteURL preferred the configuration over the request while its comment claimed the opposite, which also misdirected the logout redirect. Card layout: the footer was one two-column row shared between a label and a forty-character identifier, so the identifier took the width and crushed its label into three stacked words. Each pair is a line now, the value truncates with the full text in the title, and cards align their footers across a row.
2026-10-05 19:18:07 +02:00
<!-- The shared styling contract. Every palette, both appearances, in one
stylesheet, so switching is a change to an attribute rather than a request.
The origin is substituted by the server from GO_LOOSE_THEME_BASE_URL; when it
is unset the link is simply absent and this application's own theme blocks
take over unchanged. -->
<link rel="stylesheet" href="__GO_LOOSE_THEME_ORIGIN__/rt/v1/contract.css" data-theme-contract>
2026-10-01 23:15:57 +02:00
</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>
2026-10-04 16:57:30 +02:00
<a href="/roles">Roles</a>
2026-10-01 23:15:57 +02:00
<label class="compact">Tenant<select id="tenant-context"></select></label>
feat: account menu, database roles, and single logout Bundles the console work from the current branch: the header account control, database-backed roles and permissions, and signing out across the applications that share this sign-in. The account control replaces a separate identity chip and a separate sign-out button, which meant the header carried two controls describing one fact. web/account.js is mounted into every page by a placeholder and owns the keyboard handling, the outside-click behaviour and the sign-out form once. It also fills the trigger's name, which the previous markup left empty. Signing out revokes the hosted-login bearer sessions this installation issued, then redirects to the provider's end_session_endpoint when the discovery document advertises one. Applications that authenticate against this installation therefore lose their session too; one pointed straight at an identity provider is out of its reach and is unchanged. Roles and permissions become rows rather than a closed enum, so a tenant can extend the catalog without a migration, and a role can no longer be granted a capability that is not in it. requireSameOrigin now compares against the origin the request arrived on instead of the configured base URL, which defaulted to http://localhost:8080 and so rejected every write in a deployment that never set it. Two bugs found while verifying in a browser: - A profile picture never rendered. avatarURL built an absolute URL from the configured base URL, and the page's own img-src 'self' blocked it. The console's own pages now take a root-relative path; only /connect/userinfo, read from another origin, needs an absolute one, and it follows the request. - absoluteURL preferred the configuration over the request while its comment claimed the opposite, which also misdirected the logout redirect. Card layout: the footer was one two-column row shared between a label and a forty-character identifier, so the identifier took the width and crushed its label into three stacked words. Each pair is a line now, the value truncates with the full text in the title, and cards align their footers across a row.
2026-10-05 19:18:07 +02:00
<!-- The controls, injected rather than hand-written. They live in a shadow root
so this page's own CSS cannot reach them, and they read and write the same
data-theme and data-mode attributes the stylesheet blocks above key on, so
the values are already the ones this application was storing. -->
<bananas-theme-selector id="theme"></bananas-theme-selector>
<bananas-appearance-toggle id="mode"></bananas-appearance-toggle>
<div data-account-menu></div>
2026-10-01 23:15:57 +02:00
</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">
2026-10-04 12:24:13 +02:00
<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>
2026-10-01 23:15:57 +02:00
<div class="card-grid" id="app-list"></div>
</section>
<section class="panel" id="keys">
2026-10-04 12:24:13 +02:00
<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>
2026-10-01 23:15:57 +02:00
<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">
2026-10-04 12:24:13 +02:00
<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>
2026-10-01 23:15:57 +02:00
<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">
2026-10-04 12:24:13 +02:00
<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>
2026-10-01 23:15:57 +02:00
<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>
2026-10-04 16:57:30 +02:00
<p class="muted">Roles are per tenant and grant capabilities. <a href="/roles">Manage roles</a> to review or define your own. <a href="/profile">Open your own profile</a> to change your name and picture.</p>
2026-10-01 23:15:57 +02:00
</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>
2026-10-04 16:57:30 +02:00
<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" id="invite-role" required></select></label><p class="muted">You can only assign roles that are no more powerful than your own.</p><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" 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>
2026-10-01 23:15:57 +02:00
<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>
feat: account menu, database roles, and single logout Bundles the console work from the current branch: the header account control, database-backed roles and permissions, and signing out across the applications that share this sign-in. The account control replaces a separate identity chip and a separate sign-out button, which meant the header carried two controls describing one fact. web/account.js is mounted into every page by a placeholder and owns the keyboard handling, the outside-click behaviour and the sign-out form once. It also fills the trigger's name, which the previous markup left empty. Signing out revokes the hosted-login bearer sessions this installation issued, then redirects to the provider's end_session_endpoint when the discovery document advertises one. Applications that authenticate against this installation therefore lose their session too; one pointed straight at an identity provider is out of its reach and is unchanged. Roles and permissions become rows rather than a closed enum, so a tenant can extend the catalog without a migration, and a role can no longer be granted a capability that is not in it. requireSameOrigin now compares against the origin the request arrived on instead of the configured base URL, which defaulted to http://localhost:8080 and so rejected every write in a deployment that never set it. Two bugs found while verifying in a browser: - A profile picture never rendered. avatarURL built an absolute URL from the configured base URL, and the page's own img-src 'self' blocked it. The console's own pages now take a root-relative path; only /connect/userinfo, read from another origin, needs an absolute one, and it follows the request. - absoluteURL preferred the configuration over the request while its comment claimed the opposite, which also misdirected the logout redirect. Card layout: the footer was one two-column row shared between a label and a forty-character identifier, so the identifier took the width and crushed its label into three stacked words. Each pair is a line now, the value truncates with the full text in the title, and cards align their footers across a row.
2026-10-05 19:18:07 +02:00
<script src="/assets/account.js" defer></script>
2026-10-01 23:15:57 +02:00
<script src="/assets/app.js" defer></script>
feat: account menu, database roles, and single logout Bundles the console work from the current branch: the header account control, database-backed roles and permissions, and signing out across the applications that share this sign-in. The account control replaces a separate identity chip and a separate sign-out button, which meant the header carried two controls describing one fact. web/account.js is mounted into every page by a placeholder and owns the keyboard handling, the outside-click behaviour and the sign-out form once. It also fills the trigger's name, which the previous markup left empty. Signing out revokes the hosted-login bearer sessions this installation issued, then redirects to the provider's end_session_endpoint when the discovery document advertises one. Applications that authenticate against this installation therefore lose their session too; one pointed straight at an identity provider is out of its reach and is unchanged. Roles and permissions become rows rather than a closed enum, so a tenant can extend the catalog without a migration, and a role can no longer be granted a capability that is not in it. requireSameOrigin now compares against the origin the request arrived on instead of the configured base URL, which defaulted to http://localhost:8080 and so rejected every write in a deployment that never set it. Two bugs found while verifying in a browser: - A profile picture never rendered. avatarURL built an absolute URL from the configured base URL, and the page's own img-src 'self' blocked it. The console's own pages now take a root-relative path; only /connect/userinfo, read from another origin, needs an absolute one, and it follows the request. - absoluteURL preferred the configuration over the request while its comment claimed the opposite, which also misdirected the logout redirect. Card layout: the footer was one two-column row shared between a label and a forty-character identifier, so the identifier took the width and crushed its label into three stacked words. Each pair is a line now, the value truncates with the full text in the title, and cards align their footers across a row.
2026-10-05 19:18:07 +02:00
<!-- Loaded from the theme service rather than vendored, so a change to the controls
needs no rebuild here. A module import is a CORS request, so this deployment's
origin must be in the service's CORS_ALLOWED_ORIGINS; a same-origin
deployment needs nothing configured. -->
<script type="module" src="__GO_LOOSE_THEME_ORIGIN__/rt/v1/components.js" data-bananas-components></script>
2026-10-01 23:15:57 +02:00
</body>
</html>