SCIM UI (Phase 14 — TASK-0132)
The interaction contract for SCIM in
apps/web: the org-admin SCIM card on the Integrations surface. The protocol contract lives in ../api/scim.md (authoritative). Component map:
components/admin/ScimCard.tsx— the self-contained org-admin SCIM panel (enable / reveal-once token / test-connection / rotate / disable)app/org/[orgId]/admin/integrations/page.tsx— standalone admin route that rendersSsoTabs+ScimCard(session-gated; TASK-0133's org admin console will host the same components)lib/api.ts—scimApi(get / enable / update[rotate|disable] / test)
1. SCIM card
Mounted under the SSO section on the Integrations page. Self-contained:
takes orgId, owns its data via react-query (["orgs", orgId, "scim"]).
Server-side it is org-owner gated (the API 403s a non-owner), so the route
rendering the card is safe — the data layer enforces authorization.
States
- Loading — spinner row (
role="status"). - Error — a
role="alert"panel with the API error message. - Not enabled — an empty-state box with the enable form:
- User email domain (
data-testid="scim-user-email-domain") — used for@-less SCIM usernames; - Default role select
MEMBER|GUEST(scim-default-role); - Auto-join org workspaces checkbox (
scim-auto-join); - Enable SCIM button (
scim-enable).
- User email domain (
- Enabled — three sections:
- Reveal-once token panel (
scim-token-panel,role="alert") — shown after enable/rotate until the card unmounts: the rawscim_…token (scim-token), a copy button (scim-copy-token, aria-labelled, flips to a "Copied" state for 2 s), and the warning that the token is stored only as a SHA-256 hash and can never be retrieved again. - Summary + actions — endpoint URL (
scim-endpoint), email domain, default role, auto-join, the token hint (last 4 chars), a curl example (scim-curl), and the buttons: Edit (scim-edit), Rotate token (scim-rotate), Test connection (scim-test). - Danger zone — Disable SCIM (
scim-disable).
- Reveal-once token panel (
Interactions
- Enable →
POST /orgs/:orgId/scim→ the response carries the raw token once; the card keeps it in local state (never re-fetchable — the read returns onlytokenHint). - Test connection — the token input (
scim-test-token) is pre-filled with the revealed token after enable/rotate, so the test works immediately; pasting a token back is how the browser proves it without the server ever storing it. Result panel (scim-test-result,role="status"): green "Connected" / red "Failed"- detail.
- Rotate →
PATCH { rotate: true }→ new reveal-once token (the old one is dead server-side immediately); the old raw token in the test input is replaced. - Edit → inline form (domain nullable / role / auto-join) →
PATCHfield merge. - Disable → shared
ConfirmDialog(focus-trapped, destructive) →PATCH { disable: true }→ the token stops working immediately (protocol calls →401); the card returns to the not-enabled state. - Copy uses
navigator.clipboard(needs the clipboard permission in headless/CI — the E2E grants it); a failure falls back to a toast telling the user to copy manually.
Accessibility
- Inputs are real labelled fields (
Fieldrenders a<label>; the role/auto-join controls are labelled natively). - The reveal-once warning is
role="alert"; the test result and the loading row arerole="status"; disable uses the shared focus-trappedConfirmDialog(role="dialog"). - The toast region is
aria-live— E2E assertions scope by testid, not by the live region.
2. E2E coverage
tests/e2e/specs/scim.spec.ts (real two-origin dev stack):
- Lifecycle — a SCIM client (plain HTTP + bearer token) creates a user
with a password → browser login with the pushed password succeeds and
lands in the auto-joined org workspace → the client deactivates
(
replace active:false, with ETag) → the in-flight access token is rejected403 account_suspendedon its next call and a fresh browser login (new context) shows the "Account suspended." error (auth-error) → the DB row survives asSUSPENDEDwith the org membership kept +scim.user.created/scim.user.deactivatedaudit rows (actorType: INTEGRATION) → the user appears in theactive eq falseSCIM list. - Admin UI — enable → reveal-once panel + copy ("Copied"), endpoint +
curl example, test-connection green with the pre-filled token and red
with a wrong token, rotate → new token + old token
401, disable → confirm → empty state + protocol401.