feat(frontend): runtime config + real OIDC sign-in for the FrontX portal - #10
Merged
Conversation
Ports the prototype's env mechanism: window.__STUDIO_ENV__ (written by the already-present docker/10-runtime-env.sh at container start) wins over VITE_* build-time fallbacks; index.html loads /env.js before the bundle; public/env.js is the dev placeholder. Vite dev server gains the same-origin /cf proxy that nginx.conf.template provides in containers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
…ract Port of the prototype's dependency-free Authorization Code + PKCE S256 client onto @gears-frontx/auth AuthProvider: login/handleCallback/ refresh/logout/getIdentity/subscribe. Access token stays in memory; sessionStorage keeps the refresh + id tokens under the prototype's studio.oidc.* keys. Additions over the prototype: a state parameter on the redirect (CSRF), in-provider refresh dedup, and a static-token dev login path for the backend's static profiles. Fixed Keycloak endpoints, no discovery — deliberate, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
createFrontXApp gains auth: { provider: keycloakOidcProvider } — the
default frontxApiTransport() attaches Bearer to every REST call of the
host and all MFEs and performs one deduplicated refresh-and-retry after
a 401. AccountsApiService now targets /cf/account-management/v1 with a
single /me identity check; the header user is assembled from token
claims (name/preferred_username/email) with the /me subject id as the
fallback for opaque dev tokens. The scaffold's fake /api/accounts user
pipeline and its test helpers are removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
AuthGate wraps the app: completes the OIDC redirect callback when ?code is present (re-entrant-safe against StrictMode), scrubs the OIDC params from the URL, restores the session from the refresh token, and mounts the authenticated app only once a session exists — so Layout's fetchCurrentUser always runs with a token. LoginScreen offers SSO (+ google/github/microsoft IdP hints) and the collapsed static-token developer path, validated against /me before the session is established. Header returns to Layout with the current user and a sign-out that clears the header state and performs RP-initiated logout. Styled with the shell's Tailwind + aligned Studio palette (light DOM — ui-kit component CSS reads complete-color tokens the shell defines as HSL triplets). Verified live without an IdP: the gate renders the login screen, the SSO button navigates to the correct Keycloak authorize URL (client_id, redirect_uri, state, S256 challenge — connection refused only because no local Keycloak), the static path fails gracefully when the backend is down. The full SSO round-trip needs the compose stack (no container runtime on this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
|
Important Review available on request
Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Blocking: - treat a non-JSON 200 from the token endpoint as a rejected refresh and land every AuthGate failure on the login screen — a misconfigured issuer could previously strand the app on "Restoring session…" forever - gate the static-token dev sign-in behind import.meta.env.DEV in both the login screen and the provider; the panel, the default token string and the session-establishing branch are eliminated from production bundles Correctness: - decode JWT claims as UTF-8 (non-ASCII display names arrived mangled) - mark explicit sign-out transitions so the UI stops saying "Session expired" after the user clicks Sign out - surface OIDC ?error callbacks on the login screen and scrub them from the URL (the fragment now survives scrubbing too) - discard refresh results that resolve after logout (session epoch) - cap token-endpoint fetches with AbortSignal.timeout so a hung IdP can't hang every REST call behind the transport's getSession await - fail closed when a PKCE verifier exists without a stored state - request 'openid profile email' explicitly instead of relying on Keycloak default client scopes - getIdentity reads the ID token (access token only as dev fallback) and never triggers a network refresh - re-arm the proactive renewal timer after a transient network failure - route AuthGate/LoginScreen/Header/bootstrapEffects through the framework auth runtime (useFrontX().auth) instead of the provider singleton Cleanups: remove the dead mock-user pipeline (ApiUser/UserRole/UserExtra, notifyUserLoaded, app/user/loaded), share ACCOUNTS_API_BASE_URL with the login /me probe, log only error messages on /me failure (an AxiosError would print the bearer token), warn when the issuer falls back to the localhost default in prod, guard crypto.subtle with a readable error, fix the stale env.ts path in docker/10-runtime-env.sh. Tests: new AuthGate/LoginScreen/Header suites and provider cases for every fixed path (fake-timer renewal loop, logout-during-refresh, non-JSON responses, UTF-8 claims, prod gating); UTF-8-safe makeJwt helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
… timeouts Follow-ups from the re-review: - AuthGate no longer throws when createFrontXApp() was built without an auth plugin (the no-uikit scaffold entry shares App.tsx): it renders the app ungated with a loud console warning instead of crashing on mount - tests assert both token fetches carry an AbortSignal and that an aborted (timed out) refresh behaves like a network failure — the refresh token survives for the next attempt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
AndrejK666
pushed a commit
that referenced
this pull request
Aug 16, 2026
…tal (#10) * feat(frontend): runtime config layer + /cf dev proxy Ports the prototype's env mechanism: window.__STUDIO_ENV__ (written by the already-present docker/10-runtime-env.sh at container start) wins over VITE_* build-time fallbacks; index.html loads /env.js before the bundle; public/env.js is the dev placeholder. Vite dev server gains the same-origin /cf proxy that nginx.conf.template provides in containers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> * feat(frontend): Keycloak OIDC provider (PKCE) on the FrontX auth contract Port of the prototype's dependency-free Authorization Code + PKCE S256 client onto @gears-frontx/auth AuthProvider: login/handleCallback/ refresh/logout/getIdentity/subscribe. Access token stays in memory; sessionStorage keeps the refresh + id tokens under the prototype's studio.oidc.* keys. Additions over the prototype: a state parameter on the redirect (CSRF), in-provider refresh dedup, and a static-token dev login path for the backend's static profiles. Fixed Keycloak endpoints, no discovery — deliberate, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> * feat(frontend): wire the auth transport; /me against the real gear createFrontXApp gains auth: { provider: keycloakOidcProvider } — the default frontxApiTransport() attaches Bearer to every REST call of the host and all MFEs and performs one deduplicated refresh-and-retry after a 401. AccountsApiService now targets /cf/account-management/v1 with a single /me identity check; the header user is assembled from token claims (name/preferred_username/email) with the /me subject id as the fallback for opaque dev tokens. The scaffold's fake /api/accounts user pipeline and its test helpers are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> * feat(frontend): sign-in gate, login screen, header with sign-out AuthGate wraps the app: completes the OIDC redirect callback when ?code is present (re-entrant-safe against StrictMode), scrubs the OIDC params from the URL, restores the session from the refresh token, and mounts the authenticated app only once a session exists — so Layout's fetchCurrentUser always runs with a token. LoginScreen offers SSO (+ google/github/microsoft IdP hints) and the collapsed static-token developer path, validated against /me before the session is established. Header returns to Layout with the current user and a sign-out that clears the header state and performs RP-initiated logout. Styled with the shell's Tailwind + aligned Studio palette (light DOM — ui-kit component CSS reads complete-color tokens the shell defines as HSL triplets). Verified live without an IdP: the gate renders the login screen, the SSO button navigates to the correct Keycloak authorize URL (client_id, redirect_uri, state, S256 challenge — connection refused only because no local Keycloak), the static path fails gracefully when the backend is down. The full SSO round-trip needs the compose stack (no container runtime on this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> * fix(frontend): harden the auth slice per independent review Blocking: - treat a non-JSON 200 from the token endpoint as a rejected refresh and land every AuthGate failure on the login screen — a misconfigured issuer could previously strand the app on "Restoring session…" forever - gate the static-token dev sign-in behind import.meta.env.DEV in both the login screen and the provider; the panel, the default token string and the session-establishing branch are eliminated from production bundles Correctness: - decode JWT claims as UTF-8 (non-ASCII display names arrived mangled) - mark explicit sign-out transitions so the UI stops saying "Session expired" after the user clicks Sign out - surface OIDC ?error callbacks on the login screen and scrub them from the URL (the fragment now survives scrubbing too) - discard refresh results that resolve after logout (session epoch) - cap token-endpoint fetches with AbortSignal.timeout so a hung IdP can't hang every REST call behind the transport's getSession await - fail closed when a PKCE verifier exists without a stored state - request 'openid profile email' explicitly instead of relying on Keycloak default client scopes - getIdentity reads the ID token (access token only as dev fallback) and never triggers a network refresh - re-arm the proactive renewal timer after a transient network failure - route AuthGate/LoginScreen/Header/bootstrapEffects through the framework auth runtime (useFrontX().auth) instead of the provider singleton Cleanups: remove the dead mock-user pipeline (ApiUser/UserRole/UserExtra, notifyUserLoaded, app/user/loaded), share ACCOUNTS_API_BASE_URL with the login /me probe, log only error messages on /me failure (an AxiosError would print the bearer token), warn when the issuer falls back to the localhost default in prod, guard crypto.subtle with a readable error, fix the stale env.ts path in docker/10-runtime-env.sh. Tests: new AuthGate/LoginScreen/Header suites and provider cases for every fixed path (fake-timer renewal loop, logout-during-refresh, non-JSON responses, UTF-8 claims, prod gating); UTF-8-safe makeJwt helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> * fix(frontend): AuthGate tolerates a missing auth runtime; cover fetch timeouts Follow-ups from the re-review: - AuthGate no longer throws when createFrontXApp() was built without an auth plugin (the no-uikit scaffold entry shares App.tsx): it renders the app ungated with a loud console warning instead of crashing on mount - tests assert both token fetches carry an AbortSignal and that an aborted (timed out) refresh behaves like a network failure — the refresh token survives for the next attempt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> --------- Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech> Co-authored-by: Andrey Sysoev <andrey.sysoev@constructor.tech> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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
The first post-foundation slice for the FrontX portal (#9): a runtime config layer and a real Keycloak sign-in on the FrontX auth contract.
window.__STUDIO_ENV__(injected by the already-shippeddocker/10-runtime-env.sh) wins overVITE_*dev fallbacks;index.htmlloads/env.jsbefore the bundle; the Vite dev server gains the same-origin/cfproxy mirroringnginx.conf.template.KeycloakOidcProvider— the prototype's dependency-free Authorization Code + PKCE S256 client, reshaped onto@gears-frontx/auth'sAuthProvider(login/handleCallback/refresh/logout/getIdentity/subscribe). Access token stays in memory; refresh + id tokens under the prototype'sstudio.oidc.*sessionStorage keys. Over the prototype: astateparameter on the redirect (CSRF), in-provider refresh dedup, and a static-token developer login for the backend's static-auth profiles.createFrontXApp({ auth: { provider } }): the defaultfrontxApiTransport()attaches Bearer to every REST call of the host and all MFEs and does one deduplicated refresh-and-retry after a 401./me—AccountsApiServicenow targets/cf/account-management/v1; the header user is assembled from token claims with the/mesubject id as the opaque-token fallback. The scaffold's fake/api/accountspipeline and its test helpers are gone.AuthGate+LoginScreen+ Header — callback completion (StrictMode-safe), URL scrubbing, session restore from the refresh token, sign-in screen (SSO + IdP hints + collapsed static-token dev path validated against/mebefore establishing a session), and the header returns with the current user and RP-initiated sign-out.Verified live against the compose stack
Full checklist on
docker compose(postgres + keycloak + backend) +npm run dev:all, Mock API off:admin/studio) → Keycloak → callback → app mounts, URL scrubbed clean./meserved by the real gear through the dev proxy.config/dev.yaml.Zero console errors/warnings throughout. Static: 61 tests,
type-check,arch:deps(0 violations),npm ci-clean build.Out of scope (stated in code)
SSE auth (the framework transport is REST-only — matters for mini-chat later), OIDC discovery (fixed Keycloak endpoints, as in the prototype), deep links across login (
redirect_uriis always/).🤖 Generated with Claude Code