From 043ae009905bdc7a0975ab4723b3263cbeafebbf Mon Sep 17 00:00:00 2001 From: Charles Graham Date: Thu, 11 Jun 2026 12:24:00 -0500 Subject: [PATCH 1/3] Update Groundwork Water dependency --- cda-gui/package-lock.json | 58 +++++++++++++++++++++++++++++++-------- cda-gui/package.json | 2 +- cda-gui/src/main.jsx | 2 +- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/cda-gui/package-lock.json b/cda-gui/package-lock.json index 53cdf5b07..ada75c7e3 100644 --- a/cda-gui/package-lock.json +++ b/cda-gui/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@tanstack/react-query": "^5.64.1", - "@usace-watermanagement/groundwork-water": "^3.3.1", + "@usace-watermanagement/groundwork-water": "^3.9.0", "@usace/groundwork": "^3.14.18", "cwmsjs": "^2.3.2-2025.3.19", "dayjs": "^1.11.13", @@ -1718,23 +1718,25 @@ "dev": true }, "node_modules/@usace-watermanagement/groundwork-water": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@usace-watermanagement/groundwork-water/-/groundwork-water-3.3.1.tgz", - "integrity": "sha512-oN+/KlZ1+yQvf/7Az8ONbEDRiP/cEO8cNS4zeyS7oJROSJObhSkO1irPx8Zekjw9ziW9og7+jL4EaPBBsK1+yQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@usace-watermanagement/groundwork-water/-/groundwork-water-3.9.0.tgz", + "integrity": "sha512-pUrTg3voG8aiKqYJiihZ2kJDaH51qYrj2VSyrsVMSpeGACm0ZoV2N73z3mKCS/ujhHehWK7/63fUGjrEyutEWA==", "license": "MIT", "dependencies": { "cwmsjs": "^2.3.0-2024.12.10", "dayjs": "^1.11.11", "deepmerge": "^4.3.1", + "oidc-client-ts": "^3.5.0", "ol": "^10.0.0", "plotly.js-basic-dist": "^2.33.0", - "react-icons": "^5.3.0" + "react-icons": "^5.3.0", + "react-toastify": "^10.0.5" }, "peerDependencies": { "@tanstack/react-query": "^5.51.23", - "@usace/groundwork": "^3.10.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@usace/groundwork": "^3.14.19", + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" } }, "node_modules/@usace-watermanagement/groundwork-water/node_modules/cwmsjs": { @@ -1743,9 +1745,9 @@ "integrity": "sha512-5uPKl24caND+JR0C/3LLQ9cXmHy/lW4//PB+bykQ2kCKGvKOu8sdmeOaP3NbMGyHdsc6kGWJplZKmcsJEp0tBA==" }, "node_modules/@usace/groundwork": { - "version": "3.14.18", - "resolved": "https://registry.npmjs.org/@usace/groundwork/-/groundwork-3.14.18.tgz", - "integrity": "sha512-wGMhnLvCPO8G4JTyW3EIPNeK4M9AWN6tyht46Fw0CNPpSpO5Fxy8cMfW5UtBS0VXxl6qPAcJdg7EnxY1/heSag==", + "version": "3.15.6", + "resolved": "https://registry.npmjs.org/@usace/groundwork/-/groundwork-3.15.6.tgz", + "integrity": "sha512-0Q37NW/wsuST4Y70sPNbR4hfTycyd7MwUIcfIhYvAu1lwxpQTXSAmDPGnwlmCrmHQd4hSTmCB44IHFwyjWHM4A==", "license": "MIT", "dependencies": { "@headlessui/react": "^2.2.0", @@ -4129,6 +4131,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4689,6 +4700,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-client-ts": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", + "integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==", + "license": "Apache-2.0", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ol": { "version": "10.6.1", "resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz", @@ -5278,6 +5301,19 @@ "react-dom": ">=18" } }, + "node_modules/react-toastify": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/cda-gui/package.json b/cda-gui/package.json index 9f0faecc1..316b9f440 100644 --- a/cda-gui/package.json +++ b/cda-gui/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.64.1", - "@usace-watermanagement/groundwork-water": "^3.3.1", + "@usace-watermanagement/groundwork-water": "^3.9.0", "@usace/groundwork": "^3.14.18", "cwmsjs": "^2.3.2-2025.3.19", "dayjs": "^1.11.13", diff --git a/cda-gui/src/main.jsx b/cda-gui/src/main.jsx index 4191f3b36..95e0386c1 100644 --- a/cda-gui/src/main.jsx +++ b/cda-gui/src/main.jsx @@ -16,7 +16,7 @@ import Layout from "./components/Layout"; import LocationSearch from "./pages/LocationSearch.jsx"; // Styles -import "@usace/groundwork/dist/style.css"; +import "@usace/groundwork/dist/groundwork.css"; import "./css/index.css"; import ErrorFallback from "./pages/ErrorFallback"; import FilterExpressions from "./pages/rsql"; From 8a08ca1e9f12aa780e875e435215c1cbd0e748e6 Mon Sep 17 00:00:00 2001 From: Charles Graham Date: Thu, 11 Jun 2026 12:24:47 -0500 Subject: [PATCH 2/3] Add spec-driven Swagger auth controls --- cda-gui/src/css/swagger.css | 127 +++++++ cda-gui/src/pages/swagger-ui/index.jsx | 444 ++++++++++++++++++++++--- cda-gui/vite.config.js | 27 +- 3 files changed, 543 insertions(+), 55 deletions(-) diff --git a/cda-gui/src/css/swagger.css b/cda-gui/src/css/swagger.css index 831c8f547..1c34156c5 100644 --- a/cda-gui/src/css/swagger.css +++ b/cda-gui/src/css/swagger.css @@ -14,3 +14,130 @@ body { margin: 0; background: #fafafa; } + +.swagger-auth-bar { + align-items: center; + background: #f4f8fb; + border: 1px solid #c7d8e5; + border-radius: 6px; + display: flex; + gap: 1rem; + justify-content: space-between; + margin: 0.75rem 0 1rem; + padding: 1rem; +} + +.swagger-auth-bar > div:first-child { + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 0; +} + +.swagger-auth-bar strong { + color: #1f2933; + font-size: 1rem; +} + +.swagger-auth-bar span { + color: #394b59; + font-size: 0.95rem; +} + +.swagger-auth-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: flex-end; +} + +.swagger-auth-mode-note { + align-items: center; + color: #394b59; + display: inline-flex; + font-size: 0.9rem; +} + +.swagger-auth-mode-select { + align-items: center; + display: inline-flex; + gap: 0.45rem; +} + +.swagger-auth-mode-select span { + color: #394b59; + font-size: 0.875rem; + white-space: nowrap; +} + +.swagger-auth-mode-select select { + border: 1px solid #c7d8e5; + border-radius: 6px; + background: #ffffff; + color: #1f2933; + font-size: 0.9rem; + height: 2.35rem; + min-width: 10rem; + padding: 0 0.6rem; +} + +.swagger-auth-button { + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + height: 2.35rem; + padding: 0 1rem; +} + +.swagger-auth-button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.swagger-auth-button.primary { + background: #2563eb; + border-color: #1d4ed8; + color: #ffffff; +} + +.swagger-auth-button.primary:not(:disabled):hover { + background: #1d4ed8; +} + +.swagger-auth-button.secondary { + background: #ffffff; + border-color: #c7d8e5; + color: #1f2933; +} + +.swagger-auth-button.secondary:not(:disabled):hover { + background: #eef5fb; +} + +.swagger-auth-button.danger { + background: #b42318; + border-color: #912018; + color: #ffffff; +} + +.swagger-auth-button.danger:not(:disabled):hover { + background: #912018; +} + +.swagger-ui-host:not(.swagger-login-mode) .swagger-ui .auth-wrapper { + display: none; +} + +@media (max-width: 700px) { + .swagger-auth-bar { + align-items: stretch; + flex-direction: column; + } + + .swagger-auth-actions { + justify-content: flex-start; + } +} diff --git a/cda-gui/src/pages/swagger-ui/index.jsx b/cda-gui/src/pages/swagger-ui/index.jsx index 61f336260..a7090ab21 100644 --- a/cda-gui/src/pages/swagger-ui/index.jsx +++ b/cda-gui/src/pages/swagger-ui/index.jsx @@ -1,10 +1,138 @@ import SwaggerUIBundle from "swagger-ui-dist/swagger-ui-bundle"; import "swagger-ui-dist/swagger-ui.css"; +import "../../css/swagger.css"; -import { useEffect } from "react"; +import { + createCwmsLoginAuthMethod, + createKeycloakAuthMethod, +} from "@usace-watermanagement/groundwork-water"; +import { useEffect, useMemo, useRef, useState } from "react"; import { getBasePath } from "../../utils/base"; +function normalizeOpenIdConnectUrls(spec) { + const schemes = spec.components?.securitySchemes ?? {}; + for (const scheme of Object.values(schemes)) { + if (scheme.type === "openIdConnect" && scheme.openIdConnectUrl) { + const openIdConnectUrl = new URL(scheme.openIdConnectUrl, window.location.origin); + if (openIdConnectUrl.hostname === "auth") { + openIdConnectUrl.protocol = window.location.protocol; + openIdConnectUrl.host = window.location.host; + scheme.openIdConnectUrl = openIdConnectUrl.toString(); + } + } + } +} + +function getOpenIdConnectScheme(spec) { + const schemes = spec.components?.securitySchemes ?? {}; + return Object.values(schemes).find((scheme) => scheme.type === "openIdConnect"); +} + +function getCwmsLoginScheme(spec) { + return spec.components?.securitySchemes?.CwmsAAACacAuth; +} + +function isLoopbackHost(hostname) { + return ["localhost", "127.0.0.1", "::1"].includes(hostname); +} + +function isLocalOrigin(url) { + return isLoopbackHost(new URL(url).hostname); +} + +async function isCwmsLoginAvailable() { + try { + const response = await fetch(`${window.location.origin}/CWMSLogin`, { + cache: "no-store", + redirect: "manual", + }); + return response.type === "opaqueredirect" || response.status < 400; + } catch { + return false; + } +} + +function getKeycloakConfig(spec) { + const scheme = getOpenIdConnectScheme(spec); + if (!scheme?.openIdConnectUrl) { + return null; + } + + const openIdConnectUrl = new URL(scheme.openIdConnectUrl); + const realmMatch = openIdConnectUrl.pathname.match(/^(.*)\/realms\/([^/]+)\//); + if (!realmMatch) { + return null; + } + + const providerHint = scheme["x-kc_idp_hint"]?.values?.[0]; + const useLocalDevCredentials = isLocalOrigin(openIdConnectUrl); + return { + host: `${openIdConnectUrl.origin}${realmMatch[1]}`, + realm: realmMatch[2], + client: scheme["x-oidc-client-id"], + flow: useLocalDevCredentials ? "direct-grant" : "authorization-code-pkce", + username: useLocalDevCredentials ? "m5hectest" : undefined, + password: useLocalDevCredentials ? "m5hectest" : undefined, + redirectUri: window.location.href.split("?")[0], + postLogoutRedirectUri: window.location.href.split("?")[0], + providerHint: useLocalDevCredentials ? undefined : providerHint, + }; +} + +function removeSwaggerAuthOptions(spec) { + if (spec.components?.securitySchemes) { + spec.components.securitySchemes = {}; + } + + for (const path of Object.values(spec.paths ?? {})) { + for (const operation of Object.values(path ?? {})) { + if (operation && typeof operation === "object") { + operation.security = []; + } + } + } +} + export default function SwaggerUI() { + const [authStatus, setAuthStatus] = useState("checking"); + const [authMode, setAuthMode] = useState("custom"); + const [customAuthType, setCustomAuthType] = useState(null); + const [isOpenIdAuthReady, setIsOpenIdAuthReady] = useState(false); + const autoLoginAttemptedRef = useRef(false); + const openIdAuthMethodRef = useRef(null); + const openIdAuthConfigSignatureRef = useRef(null); + const useSwaggerLogin = authMode === "swagger"; + const cwmsAuthMethod = useMemo(() => { + const basePath = getBasePath(); + return createCwmsLoginAuthMethod({ + authUrl: `${window.location.origin}/CWMSLogin`, + authCheckUrl: `${basePath}/auth/keys`, + }); + }, []); + const openIdAuthMethod = isOpenIdAuthReady ? openIdAuthMethodRef.current : null; + const authMethod = + customAuthType === "cwms" + ? cwmsAuthMethod + : customAuthType === "openid" + ? openIdAuthMethod + : null; + const customAuthLabel = customAuthType === "cwms" ? "CWMS Login" : "CWBI Login"; + + const checkAuth = async () => { + if (!authMethod) { + setAuthStatus("anonymous"); + return; + } + + setAuthStatus("checking"); + try { + const isAuth = await authMethod.isAuth(); + setAuthStatus(isAuth ? "authenticated" : "anonymous"); + } catch { + setAuthStatus("anonymous"); + } + }; + useEffect(() => { // document.querySelector("#swagger-ui").prepend(Index) // TODO: Add page index to top of page @@ -13,55 +141,277 @@ export default function SwaggerUI() { // Begin Swagger UI call region // TODO: add endpoint that dynamic returns swagger generated doc - const ui = SwaggerUIBundle({ - url: getBasePath() + "/swagger-docs", - - dom_id: "#swagger-ui", - deepLinking: false, - presets: [SwaggerUIBundle.presets.apis], - plugins: [SwaggerUIBundle.plugins.DownloadUrl], - requestInterceptor: (req) => { - // Only alter cache behavior for same-origin API requests. External systems, - // like keycloak, may reject unexpected request changes. - const origin = window.location.origin; - const re = new RegExp(`^${origin}.*`); - if (re.test(req.url)) { - // Control browser 'fetch' behavior - req.cache = "no-store"; - // Ensure headers exist first - req.headers = req.headers ?? {}; - // Reverse/forward proxies, intermediary, service worker caches - req.headers["Cache-Control"] = "no-cache, no-store, max-age=0"; - req.headers["Pragma"] = "no-cache"; + let cancelled = false; + + async function initSwagger() { + const response = await fetch(`${getBasePath()}/swagger-docs`, { + headers: { + Accept: "application/json", + }, + }); + const spec = await response.json(); + normalizeOpenIdConnectUrls(spec); + const keycloakConfig = getKeycloakConfig(spec); + const hasCwmsLogin = getCwmsLoginScheme(spec) && (await isCwmsLoginAvailable()); + const nextCustomAuthType = hasCwmsLogin + ? "cwms" + : keycloakConfig + ? "openid" + : null; + if (customAuthType !== nextCustomAuthType) { + setCustomAuthType(nextCustomAuthType); + } + if (nextCustomAuthType === "openid" && keycloakConfig) { + const keycloakConfigSignature = JSON.stringify(keycloakConfig); + if (openIdAuthConfigSignatureRef.current !== keycloakConfigSignature) { + openIdAuthMethodRef.current = createKeycloakAuthMethod(keycloakConfig); + openIdAuthConfigSignatureRef.current = keycloakConfigSignature; } - return req; - }, - onComplete: () => { - const spec = JSON.parse(ui.spec().get("spec")); - for (const schemeName in spec.components.securitySchemes) { - const scheme = spec.components.securitySchemes[schemeName]; - if (scheme.type === "openIdConnect") { - let additionalParams = null; - let hints = scheme["x-kc_idp_hint"]; - if (hints) { - additionalParams = { - // Since getting the interface to allow users to choose - // is likely impossible, we will assume the first in the list - // is the "primary" auth system - kc_idp_hint: hints.values[0], - }; + setIsOpenIdAuthReady(true); + } else { + openIdAuthMethodRef.current = null; + openIdAuthConfigSignatureRef.current = null; + setIsOpenIdAuthReady(false); + } + if (!useSwaggerLogin) { + removeSwaggerAuthOptions(spec); + } + + if (cancelled) { + return; + } + + const ui = SwaggerUIBundle({ + spec, + dom_id: "#swagger-ui", + deepLinking: false, + presets: [SwaggerUIBundle.presets.apis], + plugins: [SwaggerUIBundle.plugins.DownloadUrl], + requestInterceptor: (req) => { + // Add a cache-busting query param... but only if it's to our api. Some + // external systems, like keycloak, don't allow random unknown parameters. + const requestUrl = new URL(req.url, window.location.origin); + if (requestUrl.hostname === "auth") { + requestUrl.protocol = window.location.protocol; + requestUrl.host = window.location.host; + req.url = requestUrl.toString(); + } + + if ( + requestUrl.origin === window.location.origin && + requestUrl.pathname.startsWith(getBasePath()) + ) { + req.credentials = "include"; + requestUrl.searchParams.set("_cb", Date.now()); + req.url = requestUrl.toString(); + + if (!useSwaggerLogin && authMethod?.token) { + req.headers["Authorization"] = `Bearer ${authMethod.token}`; } - ui.initOAuth({ - clientId: scheme["x-oidc-client-id"], - usePkceWithAuthorizationCodeGrant: true, - additionalQueryStringParams: additionalParams, - }); - break; + + // Also ask intermediaries not to serve from cache + req.headers["Cache-Control"] = "no-cache, no-store, max-age=0"; + req.headers["Pragma"] = "no-cache"; } + return req; + }, + onComplete: () => { + if (useSwaggerLogin) { + for (const schemeName in spec.components.securitySchemes) { + const scheme = spec.components.securitySchemes[schemeName]; + if (scheme.type === "openIdConnect") { + let additionalParams = null; + let hints = scheme["x-kc_idp_hint"]; + if (hints) { + additionalParams = { + // Since getting the interface to allow users to choose + // is likely impossible, we will assume the first in the list + // is the "primary" auth system + kc_idp_hint: hints.values[0], + }; + } + ui.initOAuth({ + clientId: scheme["x-oidc-client-id"], + usePkceWithAuthorizationCodeGrant: true, + additionalQueryStringParams: additionalParams, + }); + break; + } + } + } + }, + }); + } + + initSwagger(); + + return () => { + cancelled = true; + document.querySelector("#swagger-ui").innerHTML = ""; + }; + }, [authMethod, customAuthType, useSwaggerLogin]); + + useEffect(() => { + if (!authMethod) { + setAuthStatus("anonymous"); + return; + } + + let mounted = true; + authMethod + .isAuth() + .then((isAuth) => { + if (mounted) { + setAuthStatus(isAuth ? "authenticated" : "anonymous"); } - }, - }); - }, []); + }) + .catch(() => { + if (mounted) { + setAuthStatus("anonymous"); + } + }); + return () => { + mounted = false; + }; + }, [authMethod]); - return
; + useEffect(() => { + if ( + customAuthType !== "openid" || + !authMethod || + !isLoopbackHost(window.location.hostname) || + autoLoginAttemptedRef.current + ) { + return undefined; + } + + autoLoginAttemptedRef.current = true; + let mounted = true; + setAuthStatus("checking"); + authMethod + .login() + .then(() => authMethod.isAuth()) + .then((isAuth) => { + if (mounted) { + setAuthStatus(isAuth ? "authenticated" : "anonymous"); + } + }) + .catch(() => { + if (mounted) { + setAuthStatus("anonymous"); + } + }); + + return () => { + mounted = false; + }; + }, [authMethod, customAuthType]); + + const startOpenIdLogin = async () => { + if (!authMethod) { + return; + } + + await authMethod.login(); + await checkAuth(); + }; + + const isAuthenticated = authStatus === "authenticated"; + const isCheckingAuth = authStatus === "checking"; + + return ( + <> +
+
+ {useSwaggerLogin ? "Swagger Login" : customAuthLabel} + + {customAuthType === "cwms" + ? isAuthenticated + ? "Signed in with the shared CWMS session. Swagger requests will include it automatically." + : "Sign in with the district CWMS AAA login before using secured endpoints." + : isLoopbackHost(window.location.hostname) + ? isAuthenticated + ? "Signed in with the local Keycloak dev account." + : "Local Keycloak dev login uses m5hectest / m5hectest." + : "Sign in through the OpenID authorization flow for this CDA deployment."} + +
+
+ + {!useSwaggerLogin && customAuthType === "cwms" ? ( + <> + + {isAuthenticated && ( + + )} + + + ) : !useSwaggerLogin ? ( + <> + + {isAuthenticated && ( + + )} + + ) : ( + + Swagger authorization controls are enabled. + + )} +
+
+
+ + ); } diff --git a/cda-gui/vite.config.js b/cda-gui/vite.config.js index da3503371..f3fa93422 100644 --- a/cda-gui/vite.config.js +++ b/cda-gui/vite.config.js @@ -4,29 +4,40 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); + const cdaApiRoot = new URL(env.CDA_API_ROOT || "http://localhost:8081").origin; // const BASE_PATH = env?.BASE_PATH ?? "/cwms-data"; return { base: "/cwms-data", plugins: [react()], server: { proxy: { - "^/cwms-data/timeseries/.*": { - target: env.CDA_API_ROOT, + "/cwms-data/timeseries": { + target: cdaApiRoot, changeOrigin: true, secure: false, }, - "^/cwms-data/catalog/.*": { - target: env.CDA_API_ROOT, + "/cwms-data/catalog": { + target: cdaApiRoot, changeOrigin: true, secure: false, }, - "^/cwms-data/auth/.*": { - target: env.CDA_API_ROOT, + "/cwms-data/auth": { + target: cdaApiRoot, changeOrigin: true, secure: false, }, - "^/cwms-data/swagger-docs$": { - target: env.CDA_API_ROOT, + "/auth": { + target: cdaApiRoot, + changeOrigin: true, + secure: false, + }, + "/CWMSLogin": { + target: cdaApiRoot, + changeOrigin: true, + secure: false, + }, + "/cwms-data/swagger-docs": { + target: cdaApiRoot, changeOrigin: true, secure: false, }, From 73905b00641bae91bbcae1317b07338c5d0826c6 Mon Sep 17 00:00:00 2001 From: Charles Graham Date: Thu, 11 Jun 2026 12:25:13 -0500 Subject: [PATCH 3/3] Fix local Keycloak dev configuration --- compose_files/keycloak/healthcheck.sh | 2 +- compose_files/keycloak/realm.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/compose_files/keycloak/healthcheck.sh b/compose_files/keycloak/healthcheck.sh index 33e0a3980..dd51d85e2 100755 --- a/compose_files/keycloak/healthcheck.sh +++ b/compose_files/keycloak/healthcheck.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash { printf 'HEAD /auth/health/ready HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000 diff --git a/compose_files/keycloak/realm.json b/compose_files/keycloak/realm.json index 3d579b396..9a0851a22 100644 --- a/compose_files/keycloak/realm.json +++ b/compose_files/keycloak/realm.json @@ -664,7 +664,8 @@ "redirectUris": [ "https://cwms-data.test:8444/*", "https://localhost:5010/*", - "http://localhost:*" + "http://localhost:*", + "http://127.0.0.1:*" ], "webOrigins": [ "*" @@ -2322,4 +2323,4 @@ ] } ] -} \ No newline at end of file +}