2026-10-04 16:57:30 +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 · Roles< / 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
< link rel = "stylesheet" href = "__GO_LOOSE_THEME_ORIGIN__/rt/v1/contract.css" data-theme-contract >
2026-10-04 16:57:30 +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 = "/" > Console< / a >
< a href = "/docs" > API docs< / a >
< 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
< bananas-theme-selector id = "theme" > < / bananas-theme-selector >
< bananas-appearance-toggle id = "mode" > < / bananas-appearance-toggle >
< div data-account-menu > < / div >
2026-10-04 16:57:30 +02:00
< / nav >
< / header >
< main >
< section class = "hero" >
< div >
< p class = "eyebrow" > ROLE MANAGEMENT< / p >
< h1 > Who may do< br > < span > what, per tenant.< / span > < / h1 >
< p class = "lede" > A role is a named bundle of permissions. Built-in roles are shared by every tenant and cannot be edited; your own tenant can define as many more as it needs.< / p >
< / div >
< div class = "status-card" >
< div class = "pulse" > < / div >
< div > < strong id = "role-summary" > Roles< / strong > < span id = "role-tenant" > —< / span > < / div >
< code id = "role-count" > —< / code >
< / div >
< / section >
< div id = "notice" class = "notice" hidden > < / div >
< section class = "stats" id = "stats" >
< article > < span > Roles< / span > < strong id = "stat-roles" > —< / strong > < / article >
< article > < span > Built-in< / span > < strong id = "stat-system" > —< / strong > < / article >
< article > < span > Custom< / span > < strong id = "stat-custom" > —< / strong > < / article >
< article > < span > Assignable to you< / span > < strong id = "stat-assignable" > —< / strong > < / article >
< / section >
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
< section class = "panel active" id = "permissions" >
< div class = "panel-head" >
< div > < p class = "eyebrow" > CATALOG< / p > < h2 > Permissions< / h2 > < / div >
< button id = "new-permission" > Add permission< / button >
< / div >
< div class = "table-wrap" >
< table >
< thead > < tr > < th > Permission< / th > < th > Name< / th > < th > What it allows< / th > < th > Enforced< / th > < th > Roles< / th > < th > < / th > < / tr > < / thead >
< tbody id = "permission-list" > < / tbody >
< / table >
< / div >
< p class = "muted" > Every role can grant any name on this list. The nine marked < strong > enforced< / strong > are the ones this application checks before it acts, so granting one changes what the console lets you do. A permission you add here is reported to client applications through < code > /connect/userinfo< / code > , which can then enforce it themselves, but no part of this console tests for it.< / p >
< / section >
2026-10-04 16:57:30 +02:00
< section class = "panel active" id = "roles" >
< div class = "panel-head" >
< div > < p class = "eyebrow" > PERMISSION CATALOG< / p > < h2 > Roles< / h2 > < / div >
< button id = "new-role" > New role< / button >
< / div >
< div class = "table-wrap" >
< table >
< thead > < tr > < th > Role< / th > < th > Slug< / th > < th > Permissions< / th > < th > Members< / th > < th > Assignable to you< / th > < th > < / th > < / tr > < / thead >
< tbody id = "role-list" > < / tbody >
< / table >
< / div >
< p class = "muted" > You can only create and assign roles that grant no more than you already hold, so managing members can never be used to gain authority.< / p >
< / section >
< section class = "panel active" id = "permission-reference" >
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
< div class = "panel-head" > < div > < p class = "eyebrow" > HOW THEY ARE USED< / p > < h2 > Reading a permission< / h2 > < / div > < / div >
< div class = "check-list" >
< p class = "muted" > A membership resolves to exactly one role, and that role resolves to a set of permissions. Every tenant-scoped statement asks the database whether the caller holds the permission it needs, so a tenant-defined role is enforced the same way a built-in one is. Assigning a role only succeeds when every permission it grants is one you already hold, which is what stops handing out a role from becoming a way to gain authority.< / p >
2026-10-04 16:57:30 +02:00
< / div >
< / section >
< / main >
< dialog id = "role-dialog" >
< form id = "role-form" >
< div class = "dialog-head" > < h3 id = "role-dialog-title" > New role< / h3 > < button type = "button" class = "close" > × < / button > < / div >
< input type = "hidden" name = "role_id" >
< label id = "role-slug-label" > Slug< input name = "slug" required pattern = "[a-z0-9][a-z0-9-]{1,62}" placeholder = "release-manager" > < / label >
< label > Name< input name = "name" required maxlength = "100" placeholder = "Release manager" > < / label >
< label > Description < small > (optional)< / small > < textarea name = "description" maxlength = "500" placeholder = "Prepares releases." > < / textarea > < / label >
< fieldset > < legend > Permissions< / legend > < div id = "role-permissions" class = "check-list" > < / div > < / fieldset >
< p class = "muted" id = "role-hint" > < / p >
< button type = "submit" > Save role< / button >
< / form >
< / 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
< dialog id = "permission-dialog" >
< form id = "permission-form" >
< div class = "dialog-head" > < h3 > Add permission< / h3 > < button type = "button" class = "close" > × < / button > < / div >
< label > Name< input name = "name" required maxlength = "100" pattern = "[a-z0-9][a-z0-9]*(\.[a-z0-9]+)*" placeholder = "reports.export" > < small > lowercase words separated by dots< / small > < / label >
< label > Label< input name = "label" required maxlength = "100" placeholder = "Export reports" > < / label >
< label > Description < small > (optional)< / small > < textarea name = "description" maxlength = "500" placeholder = "Allows a client application to export the tenant's reports." > < / textarea > < / label >
< p class = "muted" > This becomes selectable in every role. Nothing in this console checks for it; it is reported to client applications, which can enforce it themselves.< / p >
< button type = "submit" > Add permission< / button >
< / form >
< / dialog >
< script src = "/assets/account.js" defer > < / script >
2026-10-04 16:57:30 +02:00
< script src = "/assets/roles.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
< script type = "module" src = "__GO_LOOSE_THEME_ORIGIN__/rt/v1/components.js" data-bananas-components > < / script >
2026-10-04 16:57:30 +02:00
< / body >
< / html >