V1 Web PWA · Surface walkthrough

Operator · SSO connections

A per-surface walkthrough of the V1 Web PWA operator surface: layout, states, interactions, data, and cross-references.

walked
9sections8 minread

On this page

Context. surface operator · domain operator (tenant — SSO/SAML/OIDC config) · route /operator/sso · auth signed-in · source apps/oshun/web/src/app/operator/sso/page.tsx

Last walked. 2026-06-25 operator SSO roster row-cell addendum — direct signed-in /operator/sso render, generatedAt anchor, live roster/masthead count attributes, seeded SAML/OIDC card metadata attributes, visible tenant/protocol/IdP/sync field cells, footer targets, anonymous redirect-before-render, editor create/import/edit/delete/test legs, and real BFF read/write/import/probe contracts verified in tenant-sso-config.spec.ts. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §91.

Purpose#

The tenant federation roster: one card per tenant SSO connection from GET /v1/sso, showing protocol (SAML/OIDC), status, the IdP metadata pointer, and last sync time. The roster is followed by the shipped OperatorSsoEditor, which manages SSO connections through the live admin SSO BFF: create, import metadata/discovery, edit claim mappings, delete, and run the connection test.

Entry points#

  • From /operator (hub) — operator-surface card; foot "← Operator hub" returns there
  • Cross-links from siblings/operator/audit foot row links here; /operator/tenants links here; this page links onward to /operator/tenants /operator/lms, and /operator/audit
  • From tenant onboarding — after onboarding a tenant, configuring SSO surfaces here (the empty state says "Onboard a tenant to start")
  • Direct URL — session-gated only (proxy.ts redirects sessionless → /welcome?redirect=); no operator-role gate in the proxy
  • Bookmarkcanonical = '/operator/sso'

Layout regions#

page.tsx is an async server component: await bffGet<SsoResponse>('/v1/sso') (falling back to { connections: [] }), rendered inside <LWebShell top={<LCustomerNav active="library" />}> (reuses the customer nav, Library active). Centered column maxWidth: 1080.

  • Header: LCustomerNav (Library highlighted — verify intended chrome)
  • Page anchor: outer content column exposes data-operator-sso-page and data-operator-sso-generated-at
  • Masthead: LMasthead wrapped by data-operator-sso-masthead with data-operator-sso-connection-count; visible copy is left "Operator · SSO", right ${data.connections.length} connections, kicker "Tenant federation", title "The SSO roster."
  • Roster: <div data-operator-sso-roster data-operator-sso-roster-count> with a grid of connection cards OR the empty card
  • Connection card (per row, <div data-operator-sso-card>): accent border when status === 'active'; stable attributes expose data-operator-sso-id, data-operator-sso-tenant-id, data-operator-sso-tenant-label, data-operator-sso-protocol, data-operator-sso-status, data-operator-sso-idp-metadata-url, and data-operator-sso-last-sync-iso
    • Top line: "Tenant · {tenantId}" eyebrow + tenantLabel (italic when active), right-aligned "{PROTOCOL} · {status}" (accent when active)
    • "IdP metadata · {idpMetadataUrl}" (mono, word-break: break-all)
    • "Last sync · {localized lastSyncIso | 'never'}" (serif italic)
    • Visible field cells expose data-operator-sso-field="tenant-eyebrow" | "tenant-label" | "protocol-status" | "idp-metadata" | "last-sync"
  • Editor: <OperatorSsoEditor data-sso-editor> — lists live admin SSO connections, exposes Test / Edit / Delete on each row, renders an Import metadata form for SAML XML or OIDC discovery, and renders a New/Edit connection form for tenant id, protocol, IdP entity/issuer, thumbprint, endpoints, claim mappings, flow toggles, JIT, and session/refresh lifetimes
  • Footer action row: LBtn ghost "← Operator hub" (/operator), LBtn ghost "Tenants" (/operator/tenants), LBtn ghost "LMS" (/operator/lms), LBtn primary "Audit log" (/operator/audit)

States#

  • Loading — source-verified 2026-06-25: no route-level loading.tsx; server-side await bffGet blocks the roster render, so there is no skeleton state. The client editor separately renders [data-sso-loading] while its admin list request is pending
  • Anonymous user — Playwright 2026-06-25: sessionless /operator/sso redirects to /welcome?... before [data-operator-sso-page] or [data-operator-sso-roster] render
  • Signed-in user — Playwright 2026-06-25: primed signed-in session plus operator admin bearer renders the page, generatedAt anchor, masthead/ roster count attributes, live roster cards, footer, and editor. Still session-gated only; no operator-role gate in the proxy or in-page guard
  • Empty — unit-covered: src/app/operator/sso/__tests__/page.test.tsx renders data.connections.length === 0 and asserts "No SSO connections configured. Onboard a tenant to start." plus "0 connections"
  • Populated — Playwright: real admin BFF seeds live SAML/OIDC connections and /operator/sso renders one card per connection; active cards expose accent status, stable protocol/status/id/tenant/metadata/sync attributes, and visible field cells
  • Protocol diversity — Playwright asserts both "SAML · active" and "OIDC · active" from live seeded connections
  • Status diversity — active is browser-covered; pending / disabled / error remain BFF-envelope/source-visible only and need dedicated seeded browser cases if the store can create those states
  • lastSyncIso null — Playwright asserts "Last sync · never"
  • BFF unreachable / non-2xx — unit-covered: src/app/operator/sso/__tests__/page.test.tsx mocks bffGet returning null; the page substitutes { connections: [] } and shows the empty state (no fabricated connections). Dedicated outage-route E2E is not forceable through page.route because this is a server component
  • Offline — RSC fetch (revalidate 60); operator SW policy TBD
  • Standalone PWA — renders; verify robots/no-index (no robots export here)

Interactions#

Masthead#

  • Masthead labels — non-interactive; Playwright asserts "Operator · SSO", "Tenant federation", the exact live "N connections" text, and matching masthead/roster count attributes

Connection card (per row in data.connections)#

  • Tenant eyebrow + label (text) — "Tenant · {tenantId}" + tenantLabel; non-interactive; Playwright asserts seeded SAML/OIDC attributes and visible field cells (no click into a tenant or connection-edit view)
  • Protocol · status (text) — "{PROTOCOL} · {status}"; non-interactive
  • IdP metadata URL (text) — displayed verbatim; not a link (plain text with word-break: break-all); verify whether it should be clickable to the IdP metadata document. Playwright pins SAML metadata URL verbatim and OIDC issuer-derived discovery URL
  • Last sync (text) — localized or "never"; non-interactive
  • Card — static roster card is non-interactive; edit/test/delete live in the editor row below, keyed by admin connection id

SSO editor#

  • List — loads the live admin SSO list through ssoClient.list; browser E2E bridges the request to the real BFF with an operator-admin bearer
  • Create connection — Playwright 2026-06-25 fills the New connection form for a valid OIDC connection, submits, verifies the form reset, reads the persisted connection back from the BFF, and sees the new editor row
  • Import metadata — Playwright 2026-06-26 drives the Import metadata form for SAML XML and OIDC discovery through the live POST /v1/admin/sso/import route, reloads the editor roster, and verifies parsed entity/issuer, endpoint, thumbprint/JWKS, and required email claim mapping persistence by BFF read-back
  • Edit connection — Playwright opens a prefilled SAML connection, changes the required email mapping external claim, saves, and verifies the persisted BFF store
  • Claim-mapping matrix — Playwright adds/persists all seven internal attribute slots and all four transform options through the browser editor and BFF read-back
  • Delete connection — Playwright deletes a connection and verifies the editor row disappears and the BFF store no longer lists it
  • Test connection — Playwright runs the browser Test action against real local OIDC discovery/JWKS endpoints and asserts both the reachable verdict and the fail-closed oidc-jwks: fail verdict
  • Client validation — unit-covered: OperatorSsoEditor.test.tsx asserts the required email mapping and BFF validation-error surfaces
  • "← Operator hub" (LBtn ghost, link) → /operator
  • "Tenants" (LBtn ghost, link) → /operator/tenants
  • "LMS" (LBtn ghost, link) → /operator/lms
  • "Audit log" (LBtn primary, link) → /operator/audit

Data & contracts#

  • Reads: bffGet<SsoResponse>('/v1/sso'){ generatedAt: string; connections: Connection[] } where Connection = { id, tenantId, tenantLabel, protocol: 'saml'|'oidc', status: 'active'|'pending'|'disabled', idpMetadataUrl, lastSyncIso: string | null }. On null substitutes an empty list
  • Writes: OperatorSsoEditor uses /v1/admin/sso for create/list/update/ delete, /v1/admin/sso/import for metadata/discovery import, and /v1/admin/sso/:id/test for probe results
  • Realtime: None observed.
  • Caching: RSC fetch, next.revalidate = 60, 5 s abort
  • Auth/role check: session-gated only (proxy.ts redirects sessionless → /welcome?redirect=); no operator-role gate in the proxy and no in-page guard

Cross-references#

E2E coverage#

  • apps/oshun/web/e2e/tenant-sso-config.spec.ts — direct /operator/sso signed-in render, stable page/masthead/roster/card/ field/footer anchors, generatedAt ISO attribute, live card count tied to the masthead and roster attributes, live BFF-seeded SAML and OIDC roster cards, seeded card id/tenant/label/protocol/status/metadata/sync attributes, visible tenant/protocol/IdP/sync field cells, footer targets (/operator, /operator/tenants, /operator/lms, /operator/audit), anonymous redirect-before-render, /v1/sso real-HTTP envelope tripwire, and browser editor create/import/edit/delete/test flows over the live admin SSO BFF via the operator-admin bridge.
  • apps/oshun/web/e2e/tenant-sso-crud-bff.spec.ts — pure-BFF create/list/update/test/delete, validation, metadata import, and successful/fail-closed probe contracts.
  • apps/oshun/web/e2e/tenant-sso-oidc-login-bff.spec.ts — real OIDC login/callback runtime with signed local id token, claim transforms, JIT, and failure verdicts.
  • Unit coverage: apps/oshun/web/src/app/operator/sso/__tests__/page.test.tsx covers server-component empty and BFF-null fallback branches; apps/oshun/web/src/components/operator/OperatorSsoEditor.test.tsx covers client validation and API-error rendering.

Open questions / known gaps#

  • The operator page now has a live SSO editor for create/edit/delete/test plus metadata import and claim mappings; this older read-only roster gap is closed on the operator surface. The separate tenant-admin metadata upload/discovery and enable-state-machine gaps remain journey-level product gaps
  • idpMetadataUrl is plain text, not a link — confirm whether operators should be able to open the IdP metadata document
  • No way to trigger a sync from this view; lastSyncIso is display-only — document the sync trigger path
  • Customer LCustomerNav (Library active) chrome on an operator route and no robots: { index: false } — confirm intended
  • An operator-role gate appears absent — proxy.ts only redirects sessionless users to /welcome; no role check in the proxy or page component (possible gap vs intended scope)