feat(settings): add a user settings page - #360
Open
jirhiker wants to merge 4 commits into
Open
Conversation
There was nowhere to see who you are signed in as, what roles you hold, or why a page is hidden from you — the only per-user control in the app was the theme toggle buried in the header dropdown. Adds /settings, reachable from that same dropdown, with five sections: - Profile: name, email, user id and session expiry, read from the id token. Read-only, and it says so: these live in single sign-on. - Access: roles grouped by portal, with the primary role marked, in the same vocabulary the access control provider uses. - Appearance: light, dark, or system. - Navigation: whether the sidebar collapses on the map page. - API keys: generate, rename, revoke, and a one-time reveal of a new key. Two supporting changes came with it. Colour mode gains a real "system" preference. It used to fall back to the OS setting only when nothing was stored, with no way to return to it and no response when the OS switched. The stored value is now the preference, the rendered mode is derived from it, and a media query listener keeps "system" honest. The sidebar auto-collapse on the map is now a preference rather than a rule. It is on by default, so nothing changes for anyone who does not go looking. Preferences are local to the browser and live in a small store read through useSyncExternalStore, so the settings page and the shell stay in step without another provider. The API keys section is UI only. No endpoints exist yet, so it runs on local state to settle the interaction first, and the card says on screen that the keys are not real credentials and do not survive a reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preview DeploymentPreview URL: https://preview-feat-user-settings-page-auejgdbofq-uc.a.run.app |
Three changes to the API keys section of the settings page. Field names are snake_case, matching what the API will serialise, so a real response can replace the local state without a translation layer: token_preview, created_at, expires_at, last_used_at, revoked_at. Keys now carry an expiry. A key is issued with a 90-day lifetime and the table gains an Expires column: a plain date while expiry is far off, then a warning-coloured countdown inside the 14-day warning window, then "Expired" in the error colour. An expired key is as unusable as a revoked one, so its actions disable and it sorts below the active keys. Revoked rows show no expiry, which no longer means anything for them. Both thresholds are exported constants; the API will own the real lifetime. The card is gated on the OGC.Internal group, modelled as a CapabilityGroup rather than a PortalRole: it carries no viewer/editor/admin hierarchy, never wins the primary role, and does not belong in the settings page's portal grouping. Accounts without it keep the card but see an alert naming the group to request, since a silently missing card leaves someone guessing. getPermissions previously normalised the groups claim down to portal roles, which discarded OGC.Internal before any component saw it; it and getAccessControlGroups now use normalizeAuthGroups, which carries roles and capability groups through together. Authorisation is unaffected, since canAccessResource re-filters to portal roles internally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict in the access control suite: staging added an assertion for ocotillo.asset-unassociated inside the same test this branch had reformatted. Kept both — the new assertion, in the wrapped form Biome produces here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preview DeploymentPreview URL: https://preview-feat-user-settings-page-auejgdbofq-uc.a.run.app |
The card ran the whole generate / rename / revoke flow against local component state, because no endpoint existed to run it against. `/api_key` exists now, so the card talks to it: `useApiKeys` lists, issues, renames and revokes, and every mutation invalidates the list rather than patching the cache, since a key's status and its last-used stamp both move on the server's clock. The disclaimer about keys not being real credentials is gone with the state that made it true. What replaces it is what a key actually is: it reaches the internal OGC collections and nothing else, and it is shown once, at creation, because the server keeps only the digest. `apiKeys.ts` loses the token generator and the state mutators and gains zod schemas mirroring `schemas/api_key.py`. `zNewApiKey` is the only shape carrying a token. The presentation helpers are unchanged; the snake_case field names they were rewritten for turn out to match the route exactly, so nothing translates between the two. A new dialog covers connecting ArcGIS Pro, which is the reason keys exist: Pro cannot carry an Authentik bearer token, so it authenticates with Basic and a saved login, or with a `token` request parameter when an intermediary refuses Basic. The server URL is built from the configured API base rather than hard-coded, so it is right in every environment. Note the deploy order: `/api_key` ships on OcotilloAPI's feat/api-key-management branch. Until that merges and deploys, this card shows a load error rather than a list, which is why the failure is rendered rather than swallowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preview DeploymentPreview URL: https://preview-feat-user-settings-page-auejgdbofq-uc.a.run.app |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
/settings, reachable from the user dropdown in the header. Five sections:accessControluses/api_key, gated onOGC.Internal, see the deploy order belowWhy
There was nowhere to see who you are signed in as, what roles you hold, or why a page is hidden from you. The only per-user control in the app was the theme toggle buried in the header dropdown.
Two supporting changes
Colour mode gains a real "system" preference. It used to fall back to the OS setting only when nothing was stored — no way back to it once you picked, and no response when the OS switched themes mid-session. The stored value is now the preference (
light/dark/system), the rendered mode is derived from it, and amatchMedialistener keepssystemhonest. Anything unrecognised inlocalStoragefalls back tosystem, which is what the app did before. The header dropdown gets a System item and ticks the preference rather than the resolved mode.Sidebar auto-collapse on the map is now a preference, not a rule. On by default, so nothing changes for anyone who does not go looking. Preferences are per-browser, in a small store read through
useSyncExternalStore, so the settings page and the shell stay in step without another provider — and a write in one tab moves the others via thestorageevent.API keys
This card started as local component state with a disclaimer saying so, because no endpoint existed.
/api_keyexists now, so the card talks to it.useApiKeyslists, issues, renames and revokes; every mutation invalidates the list rather than patching the cache, since a key's status and its last-used stamp both move on the server's clock rather than on anything this client does.A key reaches the internal OGC collections and nothing else, and the token appears exactly once, in the create response, because the server stores only its digest. The card says both of those where a person will read them.
src/utils/apiKeys.tsis now zod schemas mirroringschemas/api_key.py, withzNewApiKeythe only shape carrying a token. The field names are snake_case and match the route exactly, so nothing translates between the two.Keys expire. The table has an Expires column that reads differently as the date approaches: a plain date while it is far off, a warning-coloured countdown inside the 14-day window, then "Expired" in the error colour. An expired key is as unusable as a revoked one, so its actions disable and it sorts below the active keys. A revoked row shows no expiry at all, since its own expiry stopped meaning anything.
Connecting from ArcGIS Pro opens a dialog with the steps, because Pro is the reason keys exist: it cannot carry an Authentik bearer token, so it authenticates with Basic and a saved login, or with a
tokenrequest parameter when an intermediary refuses Basic. The server URL is built from the configured API base rather than hard-coded, so it is correct in whichever environment the app is pointed at.Gating on
OGC.InternalThe card is limited to accounts in the
OGC.Internalgroup — the same group/api_keygates minting on, since a key is a pre-authorized stand-in for it. Gating key creation on anything broader would let any Viewer mint themselves a credential that reaches the internal mount.OGC.Internalis modelled as aCapabilityGrouprather than added toPortalRole: it carries no viewer/editor/admin hierarchy, it should never win the primary role, and it does not belong in the Access card's portal grouping. Accounts without it keep the card and get an alert naming the group to request, rather than the card vanishing — a silently missing section is exactly what this page exists to explain. No key data renders in that state.One piece of plumbing worth a reviewer's eye:
getPermissionsnormalised the groups claim down to portal roles, which discardedOGC.Internalbefore any component could see it. It andgetAccessControlGroupsnow usenormalizeAuthGroups, which carries portal roles and capability groups through together. Authorisation is unaffected —canAccessResourcere-filters to portal roles internally, so the extra string cannot widen access anywhere.Deploy order
/api_keyships on OcotilloAPI'sfeat/api-key-managementbranch. Until that merges and deploys, the API keys card shows a load error instead of a list — including on this PR's preview, which builds against staging. That failure is rendered rather than swallowed on purpose: a card that silently showed nothing would be indistinguishable from an account with no keys.Nothing else on the page depends on that branch.
Verification
npm run typecheck— clean.accessControl, and component tests for every card — including that the token is shown once and never again, that revoke needs confirmation, that a revoked or expired key's actions are disabled, that an account withoutOGC.Internalsees the explanation and no key data, and that the ArcGIS dialog carries the environment's own server URL.AppShell.tsxhas pre-existing format drift onstaging; per-file diagnostics are unchanged and the suppression on the auto-collapse effect now sits where Biome wants it. Every new file is clean.biome check --writeinitially reformatted ~900 unrelated lines inAppShell.tsx— I rebuilt the file fromstagingand re-applied only the semantic edits, so the diff there is 37 lines.systemresolves against the OS, and the map preference actually changes whether the sidebar collapses. No console errors.🤖 Generated with Claude Code