diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77ef770e1e..f3889480f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc - name: checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v7.0.1 - name: setup java uses: actions/setup-java@v5.2.0 with: @@ -96,7 +96,7 @@ jobs: OPEN_API_TEST: "true" steps: - name: checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v7.0.1 - name: setup java uses: actions/setup-java@v5.2.0 with: @@ -118,14 +118,14 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v7.0.1 - name: Build docker image run: docker build -t cda:build-latest . build-schema-migration-image: runs-on: ubuntu-latest steps: - name: checkout code - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v7.0.1 # No uploads, we're just verifying that nothing broke the docker image. - name: Build Migration image uses: ./.github/actions/database-migration-image @@ -143,7 +143,7 @@ jobs: if: always() && (github.ref_name == 'develop') steps: - name: Badge Branch - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v7.0.1 with: ref: badges fetch-depth: 1 diff --git a/.github/workflows/web-gui-checks.yml b/.github/workflows/web-gui-checks.yml index e7056bdab1..95a02b8452 100644 --- a/.github/workflows/web-gui-checks.yml +++ b/.github/workflows/web-gui-checks.yml @@ -22,10 +22,8 @@ on: jobs: audit: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - + - uses: actions/checkout@v7.0.1 - name: install node for cda-gui audit uses: actions/setup-node@v4 with: diff --git a/Dockerfile b/Dockerfile index f85ec4c344..22931acdbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ -FROM gradle:8.5-jdk11 AS builder +FROM gradle:8.5-jdk17 AS builder USER $USER RUN --mount=type=cache,target=/home/gradle/.gradle -WORKDIR /builddir -COPY . /builddir/ RUN apt update && apt install -y curl ENV NVM_DIR="/root/.nvm" RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash @@ -12,55 +10,15 @@ SHELL ["/bin/bash", "-c"] RUN . "$NVM_DIR/nvm.sh" && nvm install $NODE_VERSION && nvm use $NODE_VERSION ENV NODE_PATH=$NVM_DIR/v$NODE_VERSION/lib/node_modules ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH -RUN gradle prepareDockerBuild --info --no-daemon - -FROM alpine:3.23.5 AS tomcat_base -RUN apk --no-cache upgrade && \ - apk --no-cache add \ - openjdk11-jre \ - curl \ - bash - - -RUN mkdir /download && \ - cd /download && \ - wget https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.121/bin/apache-tomcat-9.0.121.tar.gz && \ - echo "16494dd4745f808d3c506807b5275521fd71044d976f441d18eeeab0f5a38bc1b5344ca395292f6f26eb7612cd8c8e746d01ccdfb29893d394052d9f4b1f4c11 *apache-tomcat-9.0.121.tar.gz" > checksum.txt && \ - sha512sum -c checksum.txt && \ - tar xzf apache-tomcat-*tar.gz && \ - mv apache-tomcat-9.0.121 /usr/local/tomcat/ && \ - cd / && \ - rm -rf /usr/local/tomcat/webapps/* && \ - mkdir /usr/local/tomcat/webapps/ROOT && \ - printf "<%% response.sendRedirect(\"/cwms-data/\"); %%>\n" > /usr/local/tomcat/webapps/ROOT/index.jsp && \ - printf "User-agent: *\nAllow: /cwms-data/\nDisallow: /cwms-data/auth/\nDisallow: /cwms-data/catalog/\nDisallow: /cwms-data/timeseries/\nDisallow: /cwms-data/swagger-docs\nDisallow: /auth/\nSitemap: https://cwms-data.usace.army.mil/sitemap.xml\n" > /usr/local/tomcat/webapps/ROOT/robots.txt && \ - mkdir -p /usr/local/tomcat/conf/Catalina/localhost -# Now replace the Tomcat logging with logback -# NOTE: I have reviewed this jar in jd-gui and do not see anything malicious, packages are isolated to avoid issues -# with other code. -# Additionally, when we are not also accounting for some legacy systems, we will likely shift to -# Jetty, or Embedded Tomcat, to simplify the deployment process, making this subtitution unnecessary. -RUN cd /download && \ - wget https://repo1.maven.org/maven2/com/github/tomcat-slf4j-logback/tomcat9-slf4j-logback/9.0.120/tomcat9-slf4j-logback-9.0.120.jar && \ - echo "a24f49d57012472701172d8ec4509faa781a57a51e63b329441bdef80861e4550577fe703a333a7c5a6d5159ff14e96a16be631acef5df2ce14ec3c8cd6dae75 tomcat9-slf4j-logback-9.0.120.jar" > checksum.logback.txt && \ - sha512sum -c checksum.logback.txt -RUN cd /download && \ - cp tomcat9-slf4j-logback-9.0.120.jar /usr/local/tomcat/bin/tomcat-juli.jar && \ - rm /usr/local/tomcat/conf/logging.properties && \ - rm -rf /download -CMD ["/usr/local/tomcat/bin/catalina.sh","run"] - -FROM tomcat_base AS api +# Do this last no everything above can cache +COPY . /builddir/ +WORKDIR /builddir +RUN ./gradlew installDist -x test --info --no-daemon -COPY --from=builder /builddir/cda-gui/dist/sitemap.xml /usr/local/tomcat/webapps/ROOT/sitemap.xml -COPY --from=builder /builddir/cwms-data-api/build/docker/cda/ /usr/local/tomcat -COPY --from=builder /builddir/cwms-data-api/build/docker/context.xml /usr/local/tomcat/conf -COPY --from=builder /builddir/cwms-data-api/build/docker/server.xml /usr/local/tomcat/conf -COPY --from=builder /builddir/cwms-data-api/build/docker/setenv.sh /usr/local/tomcat/bin -COPY --from=builder /builddir/cwms-data-api/build/docker/libs/ /usr/local/tomcat/lib -COPY --from=builder /builddir/cwms-data-api/build/docker/logback.xml /logback.xml -COPY --from=builder /builddir/cwms-data-api/build/docker/logback-juli.xml /logback-juli.xml -COPY --from=builder /builddir/cwms-data-api/build/docker/app-context.xml /usr/local/tomcat/conf/Catalina/localhost/cwms-data.xml +FROM eclipse-temurin:17-jre-alpine-3.24 AS api +RUN adduser -D cda +COPY --from=builder /builddir/cwms-data-api/build/install/cwms-data-api /cda +COPY --from=builder /builddir/cwms-data-api/src/docker/logback.xml /logback.xml ENV CDA_JDBC_DRIVER="oracle.jdbc.driver.OracleDriver" ENV CDA_JDBC_URL="" @@ -83,3 +41,5 @@ ENV cwms.dataapi.access.openid.idpHint=federation-eams # used to simplify redeploy in certain contexts. Update to match - in image label ENV IMAGE_MARKER="a" EXPOSE 7000 +USER cda +CMD ["/cda/bin/cwms-data-api", "/cwms-data", "/cda/wars/ui.war"] diff --git a/access-manager-api/build.gradle b/access-manager-api/build.gradle index 9408f500d4..b569f23345 100644 --- a/access-manager-api/build.gradle +++ b/access-manager-api/build.gradle @@ -8,6 +8,7 @@ dependencies { exclude group: "org.eclipse.jetty" exclude group: "org.eclipse.jetty.websocket" } + compileOnly (libs.bundles.javalin.openapi) compileOnly(libs.swagger.core) { exclude group: "jakarta.xml.bind", module: "*" } diff --git a/access-manager-api/src/main/java/cwms/cda/spi/IdentityProvider.java b/access-manager-api/src/main/java/cwms/cda/spi/IdentityProvider.java index a878ba9068..b200797678 100644 --- a/access-manager-api/src/main/java/cwms/cda/spi/IdentityProvider.java +++ b/access-manager-api/src/main/java/cwms/cda/spi/IdentityProvider.java @@ -1,7 +1,8 @@ package cwms.cda.spi; import io.javalin.http.Context; -import io.swagger.v3.oas.models.security.SecurityScheme; +//import io.swagger.v3.oas.models.security.SecurityScheme; +import io.javalin.openapi.SecurityScheme; import java.security.Principal; diff --git a/buildSrc/src/main/groovy/cda.deps-conventions.gradle b/buildSrc/src/main/groovy/cda.deps-conventions.gradle index 597546c99e..252bd2ba01 100644 --- a/buildSrc/src/main/groovy/cda.deps-conventions.gradle +++ b/buildSrc/src/main/groovy/cda.deps-conventions.gradle @@ -13,9 +13,6 @@ repositories { excludeModule("javax.media", "jai_core") } - maven { - url = uri('https://repo.maven.apache.org/maven2') - } gradlePluginPortal() } diff --git a/buildSrc/src/main/groovy/cda.java-conventions.gradle b/buildSrc/src/main/groovy/cda.java-conventions.gradle index 8fe281bb10..cdb04a461d 100644 --- a/buildSrc/src/main/groovy/cda.java-conventions.gradle +++ b/buildSrc/src/main/groovy/cda.java-conventions.gradle @@ -14,8 +14,8 @@ eclipse { tasks.withType(JavaCompile) { options.encoding = "UTF-8" - sourceCompatibility = '11' - targetCompatibility = '11' + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { diff --git a/cda-gui/.prettierignore b/cda-gui/.prettierignore index 233182a156..9d64b684fb 100644 --- a/cda-gui/.prettierignore +++ b/cda-gui/.prettierignore @@ -1,3 +1,4 @@ dist storybook-static build +bin diff --git a/cda-gui/build.gradle b/cda-gui/build.gradle index 06e5fa0237..288ec5a814 100644 --- a/cda-gui/build.gradle +++ b/cda-gui/build.gradle @@ -2,7 +2,10 @@ import com.github.gradle.node.npm.task.NpmTask import com.github.gradle.node.npm.task.NpxTask plugins { - id 'java' + id 'base' + id 'cda.deps-conventions' + id 'cda.java-conventions' + id 'war' id 'com.github.node-gradle.node' version '7.1.0' } @@ -20,6 +23,17 @@ def cdaGuiViteMode = providers.gradleProperty('cdaGuiMode') .orElse(providers.environmentVariable('CDA_GUI_MODE')) .orElse('production') +dependencies { + compileOnly(libs.javalin.core) // TO get the transitive web-api spec + testImplementation(libs.javalin.core) + testImplementation(libs.bundles.junit) + testImplementation(libs.mockito.core) +} + +configurations { + warOutput +} + task buildGuiVite(type:NpxTask) { dependsOn npmInstall command = "vite" @@ -62,16 +76,12 @@ task runGuiDev(type: Exec) { workingDir project.layout.projectDirectory } -sourceSets { - java { - main { - resources { - srcDir(project.layout.projectDirectory.dir('dist')) - } - } - } -} - -tasks.named('processResources') { +war { dependsOn buildGui + archiveBaseName = 'cwms-data-ui' + from(project.layout.projectDirectory.dir('dist')) } + +artifacts { + warOutput tasks.war +} \ No newline at end of file diff --git a/cda-gui/package-lock.json b/cda-gui/package-lock.json index 8683fb5379..7e02fbf513 100644 --- a/cda-gui/package-lock.json +++ b/cda-gui/package-lock.json @@ -2967,18 +2967,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "26.6.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz", - "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~8.9.0" - } - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -10739,15 +10727,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/cda-gui/playwright.api-keys.config.js b/cda-gui/playwright.api-keys.config.js index 175cc8cb8f..00293c94bc 100644 --- a/cda-gui/playwright.api-keys.config.js +++ b/cda-gui/playwright.api-keys.config.js @@ -7,7 +7,7 @@ export default defineConfig({ webServer: { command: "npx vite --mode dev-cda-compose --host 127.0.0.1 --port 5178 --strictPort", - url: "http://127.0.0.1:5178/cwms-data/", + url: "http://127.0.0.1:5178", timeout: 120000, env: { VITE_CDA_API_ROOT: "/cwms-data", diff --git a/cda-gui/playwright.auth.config.js b/cda-gui/playwright.auth.config.js index f9cc0b71ce..afb44e1acc 100644 --- a/cda-gui/playwright.auth.config.js +++ b/cda-gui/playwright.auth.config.js @@ -5,6 +5,6 @@ export default defineConfig({ use: { baseURL: "http://127.0.0.1:18741", headless: true }, webServer: { command: "npx vite --mode test --host 127.0.0.1 --port 18741 --strictPort", - url: "http://127.0.0.1:18741/cwms-data/", + url: "http://127.0.0.1:18741/", }, }); diff --git a/cda-gui/playwright.data-query.config.js b/cda-gui/playwright.data-query.config.js index 2c6e6fecab..a8640e773a 100644 --- a/cda-gui/playwright.data-query.config.js +++ b/cda-gui/playwright.data-query.config.js @@ -7,7 +7,7 @@ export default defineConfig({ webServer: { command: "npx vite --mode dev-cda-compose --host 127.0.0.1 --port 5184 --strictPort", - url: "http://127.0.0.1:5184/cwms-data/", + url: "http://127.0.0.1:5184", timeout: 120000, env: { VITE_CDA_API_ROOT: "/cwms-data" }, }, diff --git a/cda-gui/playwright.user-roles.config.js b/cda-gui/playwright.user-roles.config.js index 7fc4bbaa0e..16af7a6977 100644 --- a/cda-gui/playwright.user-roles.config.js +++ b/cda-gui/playwright.user-roles.config.js @@ -6,7 +6,8 @@ export default defineConfig({ workers: 1, webServer: { command: "npm run dev:onboarding", - url: "http://127.0.0.1:18742/cwms-data/", - reuseExistingServer: true, + url: "http://127.0.0.1:18742", + //reuseExistingServer: true, + env: { VITE_CDA_API_ROOT: "/cwms-data" }, }, }); diff --git a/cda-gui/public/robots.txt b/cda-gui/public/robots.txt new file mode 100644 index 0000000000..b78d0b4ae9 --- /dev/null +++ b/cda-gui/public/robots.txt @@ -0,0 +1,8 @@ +User-agent: * +Allow: / +Disallow: /auth/ +Disallow: /cwms-data/auth/ +Disallow: /cwms-data/catalog/ +Disallow: /cwms-data/timeseries/ +Disallow: /cwms-data/swagger-docs +Sitemap: https://cwms-data.usace.army.mil/sitemap.xml \ No newline at end of file diff --git a/cda-gui/scripts/generate-sitemap.mjs b/cda-gui/scripts/generate-sitemap.mjs index a1e09a7085..a8bfc122a9 100644 --- a/cda-gui/scripts/generate-sitemap.mjs +++ b/cda-gui/scripts/generate-sitemap.mjs @@ -12,7 +12,7 @@ const outputPath = path.join(projectDir, "dist", "sitemap.xml"); const siteOrigin = ( process.env.SITE_ORIGIN ?? "https://cwms-data.usace.army.mil" ).replace(/\/+$/, ""); -const siteBasePath = (process.env.SITE_BASE_PATH ?? "/cwms-data").replace(/\/+$/, ""); +const siteBasePath = (process.env.SITE_BASE_PATH ?? "").replace(/\/+$/, ""); const urls = sitemapPaths.map((routePath) => { const normalizedPath = routePath ? `/${routePath}` : ""; diff --git a/cda-gui/src/components/GlobalErrorBoundary.jsx b/cda-gui/src/components/GlobalErrorBoundary.jsx index cf8398bbc1..a399bbfc71 100644 --- a/cda-gui/src/components/GlobalErrorBoundary.jsx +++ b/cda-gui/src/components/GlobalErrorBoundary.jsx @@ -23,7 +23,7 @@ export default class GlobalErrorBoundary extends Component {

{this.state.error?.message ?? "An unexpected error occurred."}

- + Return to CDA diff --git a/cda-gui/src/links/header-links.js b/cda-gui/src/links/header-links.js index 4bd0533132..ce6c79e86a 100644 --- a/cda-gui/src/links/header-links.js +++ b/cda-gui/src/links/header-links.js @@ -1,3 +1,5 @@ +import { getBasePath } from "../utils/base"; + export default [ { id: "home", @@ -17,7 +19,7 @@ export default [ { id: "swagger-schema", text: "Swagger Docs Schema", - href: "/swagger-docs", + href: `${getBasePath()}swagger-docs`, }, ], }, diff --git a/cda-gui/src/main.jsx b/cda-gui/src/main.jsx index 0badef7fdf..527a9c2e10 100644 --- a/cda-gui/src/main.jsx +++ b/cda-gui/src/main.jsx @@ -66,7 +66,7 @@ const router = createBrowserRouter( ], }, ], - { basename: "/cwms-data" }, + { basename: "/" }, ); ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/cwms-data-api/src/main/java/cwms/cda/servlet/SpaErrorStatusFilter.java b/cda-gui/src/main/java/cwms/cda/ui/SpaErrorStatusFilter.java similarity index 84% rename from cwms-data-api/src/main/java/cwms/cda/servlet/SpaErrorStatusFilter.java rename to cda-gui/src/main/java/cwms/cda/ui/SpaErrorStatusFilter.java index 5d575e4d2b..ecfd8827c3 100644 --- a/cwms-data-api/src/main/java/cwms/cda/servlet/SpaErrorStatusFilter.java +++ b/cda-gui/src/main/java/cwms/cda/ui/SpaErrorStatusFilter.java @@ -1,17 +1,17 @@ -package cwms.cda.servlet; +package cwms.cda.ui; import java.io.IOException; import java.util.Set; -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.annotation.WebFilter; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * Converts the error dispatch used to load known client-side routes into a successful response. diff --git a/cwms-data-api/src/test/java/cwms/cda/servlet/SpaErrorStatusFilterTest.java b/cda-gui/src/main/test/java/cwms/cda/ui/SpaErrorStatusFilterTest.java similarity index 85% rename from cwms-data-api/src/test/java/cwms/cda/servlet/SpaErrorStatusFilterTest.java rename to cda-gui/src/main/test/java/cwms/cda/ui/SpaErrorStatusFilterTest.java index 0b40f1a7ce..5c1d5f8e3c 100644 --- a/cwms-data-api/src/test/java/cwms/cda/servlet/SpaErrorStatusFilterTest.java +++ b/cda-gui/src/main/test/java/cwms/cda/ui/SpaErrorStatusFilterTest.java @@ -1,4 +1,4 @@ -package cwms.cda.servlet; +package cwms.cda.ui; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.mockito.Mockito.mock; @@ -7,13 +7,13 @@ import static org.mockito.Mockito.when; import java.io.IOException; -import javax.servlet.DispatcherType; -import javax.servlet.FilterChain; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebFilter; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterChain; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -51,7 +51,7 @@ void registersForIndexErrorDispatches() { "/user-roles/mvsc/" }) void returnsOkForClientRoutes(String route) throws ServletException, IOException { - HttpServletRequest request = buildRequest("GET", "/cwms-data" + route); + HttpServletRequest request = buildRequest("GET", route); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -63,7 +63,7 @@ void returnsOkForClientRoutes(String route) throws ServletException, IOException @Test void returnsOkForHeadRequest() throws ServletException, IOException { - HttpServletRequest request = buildRequest("HEAD", "/cwms-data/swagger-ui"); + HttpServletRequest request = buildRequest("HEAD", "/swagger-ui"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -75,7 +75,7 @@ void returnsOkForHeadRequest() throws ServletException, IOException { @Test void returnsOkForAlternateContextPath() throws ServletException, IOException { - HttpServletRequest request = buildRequest("GET", "/spk-data/swagger-ui", "/spk-data"); + HttpServletRequest request = buildRequest("GET", "/cwms-data-ui/swagger-ui", "/cwms-data"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -87,7 +87,7 @@ void returnsOkForAlternateContextPath() throws ServletException, IOException { @Test void preservesNotFoundStatusForUnknownRoutes() throws ServletException, IOException { - HttpServletRequest request = buildRequest("GET", "/cwms-data/not-a-client-route"); + HttpServletRequest request = buildRequest("GET", "not-a-client-route"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); @@ -99,7 +99,7 @@ void preservesNotFoundStatusForUnknownRoutes() throws ServletException, IOExcept @Test void preservesNotFoundStatusForNonPageRequests() throws ServletException, IOException { - HttpServletRequest request = buildRequest("POST", "/cwms-data/swagger-ui"); + HttpServletRequest request = buildRequest("POST", "/swagger-ui"); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain chain = mock(FilterChain.class); diff --git a/cda-gui/src/main/webapp/WEB-INF/web.xml b/cda-gui/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..7b2a86eb00 --- /dev/null +++ b/cda-gui/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + CWMS Data API UI + + + index.html + + + + + 404 + /index.html + + \ No newline at end of file diff --git a/cda-gui/src/pages/decide/index.jsx b/cda-gui/src/pages/decide/index.jsx deleted file mode 100644 index c78aa3b56a..0000000000 --- a/cda-gui/src/pages/decide/index.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect } from "react"; - -function Decide() { - useEffect(() => { - document.title = "CWMS Public Services"; - }); - return ( -
-

- Hello, you've successfully reach the Corps Water Management System (CWMS) - public data system. Multiple sites are available please follow one of the links - below for access to our data: -

- -
- ); -} - -export default Decide; diff --git a/cda-gui/src/utils/base.js b/cda-gui/src/utils/base.js index 2386fa9545..c42e095436 100644 --- a/cda-gui/src/utils/base.js +++ b/cda-gui/src/utils/base.js @@ -1,8 +1,7 @@ // base.js export function getBasePath() { - const { pathname } = window.location; - const basePath = "/" + pathname.split("/")[1]; - return basePath; + const configuredRoot = import.meta.env.VITE_CDA_API_ROOT || "/cwms-data"; + return new URL(configuredRoot, window.location.origin).pathname.replace(/\/$/, ""); } export function getOrigin() { diff --git a/cda-gui/tests/auth/cwms-login.spec.js b/cda-gui/tests/auth/cwms-login.spec.js index 29bcfd17a9..b1caf6b7f1 100644 --- a/cda-gui/tests/auth/cwms-login.spec.js +++ b/cda-gui/tests/auth/cwms-login.spec.js @@ -10,7 +10,7 @@ async function mockDeployment(page, schemes = { CwmsAAACacAuth: cwmsScheme }) { await page.route("**/swagger-docs", (route) => route.fulfill({ json: { - openapi: "3.0.3", + openapi: "3.1.0", info: { title: "CWMS authentication test", version: "1" }, paths: {}, components: { securitySchemes: schemes }, @@ -46,7 +46,7 @@ async function mockDeployment(page, schemes = { CwmsAAACacAuth: cwmsScheme }) { test("header CWMS login and logout return to the current page", async ({ page }) => { await mockDeployment(page); - await page.goto("/cwms-data/regexp/?office=SWT#login"); + await page.goto("/regexp/?office=SWT#login"); const originalLocation = page.url(); const loginRequest = page.waitForRequest("**/CWMSLogin/login?*"); @@ -71,18 +71,18 @@ test("header CWMS login and logout return to the current page", async ({ page }) test("Swagger CWMS login also establishes the header session", async ({ page }) => { await mockDeployment(page); - await page.goto("/cwms-data/swagger-ui"); + await page.goto("/swagger-ui"); await page.getByRole("button", { name: "Sign in", exact: true }).click(); await expect( - page.getByRole("button", { name: "Log out", exact: true }), + page.getByRole("button", { name: "Sign out", exact: true }), ).toBeVisible(); - await expect(page).toHaveURL(/\/cwms-data\/swagger-ui$/); + await expect(page).toHaveURL(/\/swagger-ui$/); }); test("unsupported authentication still reports sign-in unavailable", async ({ page, }) => { await mockDeployment(page, {}); - await page.goto("/cwms-data/"); + await page.goto("/"); await expect(page.getByText("Sign-in unavailable", { exact: true })).toBeVisible(); }); diff --git a/cda-gui/tests/auth/token-refresh.spec.js b/cda-gui/tests/auth/token-refresh.spec.js index d828bf48ff..71549518e9 100644 --- a/cda-gui/tests/auth/token-refresh.spec.js +++ b/cda-gui/tests/auth/token-refresh.spec.js @@ -37,7 +37,7 @@ test("published user hooks search across offices, assign membership, and save a user.roles.SWT = [...new Set([...(user.roles.SWT ?? []), ...roles])]; return route.fulfill({ status: 204 }); }); - await page.goto("/cwms-data/user-roles"); + await page.goto("/user-roles"); await page.getByRole("button", { name: "Assign office", exact: true }).click(); const dialog = page.getByRole("dialog"); await dialog @@ -134,7 +134,7 @@ async function mockDeployment(page, flow) { specRequests += 1; return route.fulfill({ json: { - openapi: "3.0.3", + openapi: "3.1.0", info: { title: "Refresh test", version: "1" }, servers: [{ url: `${origin}/cwms-data` }], components: { @@ -193,7 +193,7 @@ for (const flow of ["direct-grant", "pkce"]) { page.on("framenavigated", (frame) => { if (frame === page.mainFrame()) navigations += 1; }); - await page.goto("/cwms-data/swagger-ui"); + await page.goto("/swagger-ui"); await expect( page.getByRole("button", { name: "Log out", exact: true }), ).toBeVisible(); @@ -257,7 +257,7 @@ test("the application preserves a user-list form during background refresh", asy }); return route.fulfill({ json: {} }); }); - await page.goto("/cwms-data/user-lists"); + await page.goto("/user-lists"); await page.getByRole("button", { name: "New list", exact: true }).click(); await page.getByRole("textbox", { name: "List ID", exact: true }).fill("DRAFT-LIST"); await page @@ -282,7 +282,7 @@ test("an expired OpenID session leaves the Swagger draft available", async ({ page, }) => { const deployment = await mockDeployment(page, "pkce"); - await page.goto("/cwms-data/swagger-ui"); + await page.goto("/swagger-ui"); await expect( page.getByRole("button", { name: "Log out", exact: true }), ).toBeVisible(); diff --git a/cda-gui/tests/data-query/location-pagination.spec.js b/cda-gui/tests/data-query/location-pagination.spec.js index 07c713f9ba..32195f019a 100644 --- a/cda-gui/tests/data-query/location-pagination.spec.js +++ b/cda-gui/tests/data-query/location-pagination.spec.js @@ -19,7 +19,7 @@ async function searchLocations(page, pageCount) { }, }); }); - await page.goto("/cwms-data/data-query"); + await page.goto("/data-query"); await page.getByRole("button", { name: "Guided", exact: true }).click(); await page .locator("select") diff --git a/cda-gui/tests/user-roles/office-url.spec.js b/cda-gui/tests/user-roles/office-url.spec.js index 5dc958401b..0ac6bd10ea 100644 --- a/cda-gui/tests/user-roles/office-url.spec.js +++ b/cda-gui/tests/user-roles/office-url.spec.js @@ -15,7 +15,7 @@ test("office links override storage; dropdown labels are sorted and include HQ", JSON.stringify({ office: "SPK", userName: "gray.sacramento" }), ), ); - await login(page, "/cwms-data/user-roles/swt"); + await login(page, "/user-roles/swt"); await expect(page.locator("#role-office")).toHaveValue("SWT"); await expect(page.locator("#role-office option")).toHaveText([ "HQ - Headquarters", @@ -31,7 +31,7 @@ test("office links override storage; dropdown labels are sorted and include HQ", }); test("HQ is viewable without granting HQ edit permissions", async ({ page }) => { - await login(page, "/cwms-data/user-roles/hq"); + await login(page, "/user-roles/hq"); await expect(page.locator("#role-office")).toHaveValue("HQ"); await expect( page.getByText( @@ -58,14 +58,14 @@ test("HQ administrators retain edit access and HQ occurs only once", async ({ }, }), ); - await login(page, "/cwms-data/user-roles/HQ"); + await login(page, "/user-roles/HQ"); await expect(page).toHaveURL(/\/user-roles\/hq$/); await expect(page.locator('#role-office option[value="HQ"]')).toHaveCount(1); await expect(page.locator("#role-mode-custom")).toBeEnabled(); }); test("unknown office URLs fall back to the default office", async ({ page }) => { - await login(page, "/cwms-data/user-roles/unknown"); + await login(page, "/user-roles/unknown"); await expect(page).toHaveURL(/\/user-roles\/spk$/); await expect(page.locator("#role-office")).toHaveValue("SPK"); }); diff --git a/cda-gui/tests/user-roles/onboarding.spec.js b/cda-gui/tests/user-roles/onboarding.spec.js index 59c85cd04a..d420c6b306 100644 --- a/cda-gui/tests/user-roles/onboarding.spec.js +++ b/cda-gui/tests/user-roles/onboarding.spec.js @@ -6,7 +6,7 @@ test.beforeEach(async ({ request }) => { }); async function openRoles(page) { - await page.goto("/cwms-data/user-roles"); + await page.goto("/user-roles"); const login = page.getByRole("button", { name: "Log in", exact: true }); await login.last().click(); await expect( @@ -130,7 +130,7 @@ test("missing PD hides office assignment while retaining the office role editor" await page.route("**/demo-api/user/profile*", (route) => route.fulfill({ json: { ...demoProfile, roles: { SWT: ["CWMS User Admins"] } } }), ); - await page.goto("/cwms-data/user-roles"); + await page.goto("/user-roles"); await page.getByRole("button", { name: "Log in", exact: true }).last().click(); await expect(page.getByText("Office users", { exact: true })).toBeVisible(); await expect( diff --git a/cda-gui/tests/user-roles/selection.spec.js b/cda-gui/tests/user-roles/selection.spec.js index a825b008f2..32e95d5fc5 100644 --- a/cda-gui/tests/user-roles/selection.spec.js +++ b/cda-gui/tests/user-roles/selection.spec.js @@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test"; import { demoProfile, demoRoles } from "./demo-data"; async function openRoles(page) { - await page.goto("/cwms-data/user-roles"); + await page.goto("/user-roles"); await page.getByRole("button", { name: "Log in", exact: true }).last().click(); } diff --git a/cda-gui/vite.config.js b/cda-gui/vite.config.js index 3b1dcfd19a..a6112a3964 100644 --- a/cda-gui/vite.config.js +++ b/cda-gui/vite.config.js @@ -15,9 +15,8 @@ export default defineConfig(({ mode }) => { env.VITE_CDA_API_ROOT || env.CDA_API_ROOT || "http://localhost:8081", "http://localhost:8081", ).origin; - // const BASE_PATH = env?.BASE_PATH ?? "/cwms-data"; return { - base: "/cwms-data", + base: "/", plugins: [react()], optimizeDeps: { include: ["react-dom/client", "react-router-dom"], diff --git a/clients/typescript/build.gradle b/clients/typescript/build.gradle index 245340aa66..3a54d4ac3c 100644 --- a/clients/typescript/build.gradle +++ b/clients/typescript/build.gradle @@ -6,7 +6,7 @@ import org.openapitools.generator.gradle.plugin.tasks.ValidateTask plugins { id 'base' id 'com.github.node-gradle.node' version '7.1.0' - id 'org.openapi.generator' version '7.15.0' + id 'org.openapi.generator' version '7.25.0' } def npmCacheDir = layout.buildDirectory.dir("npm-cache") @@ -40,11 +40,12 @@ clean.dependsOn cleanGeneration tasks.register('copyOpenApiSpec') { group 'openapi' description 'Copy the locally generated CDA OpenAPI spec into the TypeScript client workspace.' - dependsOn ':cwms-data-api:executeOpenAPIConversion' + dependsOn ':cwms-data-api:compileJava' + inputs.file "$rootDir/cwms-data-api/build/classes/java/main/openapi-plugin/openapi-default.json" outputs.file rawSpecFile doLast { copy { - from "$rootDir/cwms-data-api/build/openapi.json" + from "$rootDir/cwms-data-api/build/classes/java/main/openapi-plugin/openapi-default.json" into projectDir rename { 'cwms-swagger-raw.json' } } @@ -68,7 +69,7 @@ tasks.register('validateOpenApiSpec', ValidateTask) { group 'openapi' description 'Validate the adjusted TypeScript client OpenAPI spec.' dependsOn modSpec - inputSpec.set(modifiedSpecFile.asFile.absolutePath) + inputSpec.set(modifiedSpecFile.asFile) } tasks.register('generateTypeScriptClient', GenerateTask) { @@ -76,9 +77,9 @@ tasks.register('generateTypeScriptClient', GenerateTask) { description 'Generate the cwmsjs TypeScript client with the Gradle OpenAPI plugin.' dependsOn validateOpenApiSpec validateSpec = true - inputSpec = modifiedSpecFile.asFile.absolutePath - outputDir = generatedClientDir.asFile.absolutePath - configFile = layout.projectDirectory.file('openapi.config.json').asFile.absolutePath + inputSpec = modifiedSpecFile.asFile + outputDir = generatedClientDir.asFile + configFile = layout.projectDirectory.file('openapi.config.json').asFile generatorName = 'typescript-fetch' globalProperties.set([ modelDocs : 'true', @@ -87,7 +88,7 @@ tasks.register('generateTypeScriptClient', GenerateTask) { apiTests : 'false', enabledPostProcessFile: 'false' ]) - ignoreFileOverride = layout.projectDirectory.file('.openapi-generator-ignore').asFile.absolutePath + ignoreFileOverride = layout.projectDirectory.file('.openapi-generator-ignore').asFile } tasks.register('modPackage', NpxTask) { diff --git a/clients/typescript/openapitools.json b/clients/typescript/openapitools.json index 9cbc6d5d64..8dd0d74557 100644 --- a/clients/typescript/openapitools.json +++ b/clients/typescript/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "5.4.0" + "version": "7.25.0" } } diff --git a/clients/typescript/package-lock.json b/clients/typescript/package-lock.json index 50bf84662b..bafdba66e2 100644 --- a/clients/typescript/package-lock.json +++ b/clients/typescript/package-lock.json @@ -327,6 +327,7 @@ "integrity": "sha512-kEGSzqM2lWr4whh4Ubflw+oPZSEzxvRMu9WL+LveZploJWTjec5bBlCiRVlVzTPg2kIwBiLwWSvCCW7Wnin1gg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -931,6 +932,7 @@ "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -3992,7 +3994,8 @@ "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/randomatic": { "version": "3.1.1", @@ -4336,7 +4339,8 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/regenerator-runtime": { "version": "0.10.5", @@ -4524,6 +4528,7 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -5368,6 +5373,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/clients/typescript/scripts/spec-updates/BaseRatingMetadata.json b/clients/typescript/scripts/spec-updates/BaseRatingMetadata.json index 1af7eaf76a..c1bf5a3dc3 100644 --- a/clients/typescript/scripts/spec-updates/BaseRatingMetadata.json +++ b/clients/typescript/scripts/spec-updates/BaseRatingMetadata.json @@ -30,7 +30,7 @@ "type": "string" }, "vertical-datum-info": { - "$ref": "#/components/schemas/vertical-datum-info" + "$ref": "#/components/schemas/VerticalDatumInfo" } } } diff --git a/compose_files/api_entry.sh b/compose_files/api_entry.sh index 97c9a818c1..726ab368a6 100755 --- a/compose_files/api_entry.sh +++ b/compose_files/api_entry.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh nohup ./proxy_auth.sh 2>&1 > /dev/null & echo "auth proxy started now executing $*" diff --git a/compose_files/proxy_auth.sh b/compose_files/proxy_auth.sh index e57c30aa89..a322655155 100755 --- a/compose_files/proxy_auth.sh +++ b/compose_files/proxy_auth.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh mkfifo backpipe #while true; do nc -lk -p 7100 0backpipe; done diff --git a/cwms-data-api/build.gradle b/cwms-data-api/build.gradle index 9225da419c..10138ac5e2 100644 --- a/cwms-data-api/build.gradle +++ b/cwms-data-api/build.gradle @@ -1,13 +1,13 @@ plugins { id 'cda.deps-conventions' id 'cda.java-conventions' - id 'war' id 'com.gradleup.shadow' version '8.3.10' + id 'application' } configurations { info - webjars + ui docker baseLibs tomcatLibs @@ -16,10 +16,6 @@ configurations { jooqCodegenLegacy } -configurations.implementation { - exclude group: 'com.oracle.database.jdbc' -} - configurations.all { exclude group: 'dom4j' exclude group: 'org.apache.xmlbeans' @@ -30,6 +26,7 @@ configurations.all { dependencies { annotationProcessor(libs.com.google.auto.service) + annotationProcessor(libs.javalin.openapi.annotation.processor) compileOnly(libs.com.google.auto.service.annotations) implementation(libs.jaxb.api) implementation(libs.jaxb.core) @@ -79,6 +76,8 @@ dependencies { //exclude group: "org.python", module: "jython-standalone" //exclude group: "mil.army.usace.hec.swingx" exclude group: "*" + exclude group: "com.oracle", module: "*" + exclude group: "com.oracle.database.jdbc", module: "*" } implementation(libs.google.flogger.api) @@ -88,18 +87,24 @@ dependencies { implementation(libs.nucleus.data) { exclude group: "org.jdom" + exclude group: "com.oracle", module: "*" + exclude group: "com.oracle.database.jdbc", module: "*" } implementation(libs.nucleus.metadata) implementation(libs.cwms.ratings.core) { //exclude group: "mil.army.usace.hec.swingx" exclude group: "*" + exclude group: "com.oracle", module: "*" + exclude group: "com.oracle.database.jdbc", module: "*" } implementation(libs.cwms.ratings.io.xml) implementation(libs.cwms.ratings.io.jdbc) { exclude group: "mil.army.usace.hec", module: "cwms-db-jooq-codegen" exclude group: "org.jooq.pro-java-11", module: "jooq" + exclude group: "com.oracle", module: "*" + exclude group: "com.oracle.database.jdbc", module: "*" } implementation(libs.jooq) { @@ -113,23 +118,17 @@ dependencies { implementation(libs.geojson.jackson) implementation(libs.jackson.datatype.jdk8) + + implementation(libs.com.zaxxer.hikaricp) + implementation(libs.javalin.core) + implementation(libs.bundles.javalin.openapi) - implementation(libs.javalin.core) { - exclude group: "org.eclipse.jetty" - exclude group: "org.eclipse.jetty.websocket" - } - implementation(libs.javalin.openapi) { - //exclude group: "org.eclipse.jetty" - exclude group: "org.eclipse.jetty.websocket" - exclude group: "jakarta.xml.bind", module: "*" - } implementation(libs.swagger.core) { exclude group: "jakarta.xml.bind", module: "*" } implementation(libs.password4j) implementation 'cz.jirutka.rsql:rsql-parser:2.1.0' - compileOnly(libs.javaee.web.api) compileOnly(libs.cwms.tomcat.auth) @@ -154,27 +153,24 @@ dependencies { testImplementation(libs.mockito.core) testRuntimeOnly(libs.jaxb.impl) testImplementation(libs.openapi.spec.validation) + testImplementation(libs.javalin.test.tools) + testImplementation(libs.javalin.context.mock) { + exclude group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api" + } - baseLibs(libs.oracle.jdbc.driver) - - compileOnly(libs.oracle.jdbc.driver) - testRuntimeOnly(libs.oracle.jdbc.driver) + implementation(libs.oracle.jdbc.driver) testImplementation(libs.bundles.jackson) testImplementation(libs.rest.assured) testImplementation(libs.hamcrest.all) testImplementation(libs.slf4j.jul) - tomcatLibs(libs.bundles.tomcat.embedded) - tomcatLibs(libs.bundles.tomcat.support) - tomcatLibs(libs.google.flogger.api) - tomcatLibs(libs.google.flogger.system.backend) - baseLibs(libs.google.flogger.slf4j.backend) { exclude group: "org.slf4j" } - baseLibs(libs.ch.qos.logback) + implementation(libs.ch.qos.logback) + //baseLibs(libs.ch.qos.logback) baseLibs(libs.io.opentelemetry.instrumentation.logback.mdc) testImplementation(libs.bundles.testcontainers) @@ -183,7 +179,7 @@ dependencies { testImplementation(libs.cwms.tomcat.auth) testImplementation(libs.apache.freemarker) - webjars(project(":cda-gui")) + ui(project(path: ":cda-gui", configuration: "warOutput")) // override versions @@ -194,95 +190,48 @@ dependencies { implementation(libs.minio) } -task extractWebJars(type: Copy) { - dependsOn configurations.webjars - from zipTree(configurations.webjars.singleFile) - into file("$buildDir/extra") - includeEmptyDirs false - includeEmptyDirs = false -} - -war { - dependsOn extractWebJars - from "$buildDir/extra" - from "src/resources/" - - manifest { - attributes "build-version": project.version - } - - doLast { - println(war.archiveFileName.toString()) - println("::set-output name=WARFILE::${project.name}-${project.version}.war") - } -} - -compileTestJava.classpath += configurations.tomcatLibs - -sourceSets { - formatList { - output.resourcesDir = layout.buildDirectory.dir("generated/sources/annotationProcessor/java/main") - java.destinationDirectory = layout.buildDirectory.dir("generated/sources/annotationProcessor/java/main") - } - main { - runtimeClasspath += formatList.runtimeClasspath - } - test { - runtimeClasspath += formatList.runtimeClasspath - } -} - test { useJUnitPlatform() { excludeTags "integration" } - classpath += configurations.tomcatLibs + classpath += configurations.baseLibs + jvmArgs += "-Dlogback.configurationFile=${projectDir}/logback-tests.xml" } description = 'CWMS-Data-API' -task generateConfig(type: Copy) { - doFirst { - project.delete "$buildDir/tomcat/" - project.mkdir "$buildDir/tomcat/logs" - } - from 'src/test/resources/tomcat' - into "$buildDir/tomcat" - - //rename "context.xml", "context.xml.template" - rename "tomcat-server.xml", "server.xml" - - inputs.dir "src/test/resources" - outputs.dir "$buildDir/tomcat/conf" +application { + mainClass = 'cwms.cda.CwmsDataApi' + applicationDefaultJvmArgs += "-Dlogback.configurationFile=logback.xml" } -task run(type: JavaExec) { - group "application" - dependsOn generateConfig - dependsOn war +distributions { + main { + contents { + into ('wars') { + from configurations.ui + rename '.*', 'ui.war' + } + } + } +} - classpath += configurations.baseLibs - classpath += configurations.tomcatLibs - classpath += sourceSets.test.output // to get the fixture +run() { + dependsOn configurations.ui + classpath sourceSets.main.runtimeClasspath + + configurations.baseLibs - mainClass = "fixtures.TomcatServer" - systemProperties += project.properties.findAll { k, v -> k.startsWith("RADAR") } + mainClass = "cwms.cda.CwmsDataApi" systemProperties += project.properties.findAll { k, v -> k.startsWith("CDA") } systemProperties += project.properties.findAll { k, v -> k.startsWith("cwms") } - def context = project.findProperty("cda.war.context") ?: "spk-data" + def context = project.findProperty("cda.war.context") ?: "/cwms-data" - args "$buildDir/tomcat", "$buildDir/libs/${project.name}-${project.version}.war", context - jvmArgs += "-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" - jvmArgs += "-Djava.util.logging.config.file=$projectDir/logging.properties" - jvmArgs += "-DTOMCAT_RESOURCES=$buildDir/tomcat/conf/context.xml" - jvmArgs += "-Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource" + args context, configurations.ui.asPath, 7010 jvmArgs += "-Dlogback.configurationFile=${projectDir}/logback.xml" - jvmArgs += "-Dcatalina.base=$buildDir/tomcat" - jvmArgs += "-DwarContext=/" + context - jvmArgs += "-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true" jvmArgs += "-DAUTH_RE_ENABLE_NON_HASH_KEY_SUPPORT=true" jvmArgs += "-Duser.timezone=UTC" + // If you have the docker-compose environment up and are trying to run // CDA from run to debug uncomment the following lines. //jvmArgs += "-Dcwms.dataapi.access.providers=KeyAccessManager,CwmsAccessManager,OpenID" @@ -302,13 +251,11 @@ tasks.register('seedDevApiKeys', JavaExec) { mainClass = "fixtures.SeedDevApiKeys" systemProperties += project.properties.findAll { k, v -> k.startsWith("CDA") } -// jvmArgs += "-Djava.util.logging.config.file=$projectDir/logging.properties" } task integrationTests(type: Test) { -// dependsOn test - dependsOn generateConfig - dependsOn war + dependsOn test + dependsOn configurations.ui useJUnitPlatform() { includeTags "integration" @@ -316,27 +263,20 @@ task integrationTests(type: Test) { shouldRunAfter test classpath += configurations.baseLibs // The before all extension will take care of these properties - systemProperties += project.properties.findAll { k, v -> k.startsWith("RADAR") && !k.startsWith("RADAR_JDBC") } systemProperties += project.properties.findAll { k, v -> k.startsWith("CDA") && !k.startsWith("CDA_JDBC") } systemProperties += project.properties.findAll { k, v -> k.startsWith("testcontainer") } - jvmArgs += "-DwarFile=$buildDir/libs/${project.name}-${project.version}.war" - jvmArgs += "-DwarContext=/cwms-data" - jvmArgs += "-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" - jvmArgs += "-Djava.util.logging.config.file=$projectDir/logging.properties" + jvmArgs += "-DuiWarFile=${configurations.ui.asPath}" + jvmArgs += "-DwarContext=/cwms-data" jvmArgs += "-Dlogback.configurationFile=${projectDir}/logback.xml" - jvmArgs += "-Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource" + jvmArgs += "-Dcwms.dataapi.access.provider=MultipleAccessManager" jvmArgs += "-Dcwms.dataapi.access.providers=KeyAccessManager,CwmsAccessManager" jvmArgs += "-Dproperties.file=$projectDir/src/test/resources/features-integration.properties" - jvmArgs += "-Dcatalina.base=$buildDir/tomcat" - //jvmArgs += "-Dflogger.backend_factory=com.google.common.flogger.backend.slf4j.Slf4jBackendFactory#getInstance" } task timeseriesReadBenchmark(type: JavaExec) { group "verification" description = "Run the local time-series read benchmark harness" - dependsOn generateConfig - dependsOn war dependsOn testClasses workingDir = projectDir @@ -352,41 +292,9 @@ task timeseriesReadBenchmark(type: JavaExec) { jvmArgs += "-DwarFile=$buildDir/libs/${project.name}-${project.version}.war" jvmArgs += "-DwarContext=/cwms-data" - jvmArgs += "-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" - jvmArgs += "-Djava.util.logging.config.file=$projectDir/logging.properties" - jvmArgs += "-Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource" + jvmArgs += "-Dlogback.configurationFile=${projectDir}/logback.xml" jvmArgs += "-Dcwms.dataapi.access.provider=MultipleAccessManager" jvmArgs += "-Dcwms.dataapi.access.providers=KeyAccessManager,CwmsAccessManager" - jvmArgs += "-Dcatalina.base=$buildDir/tomcat" -} - -task prepareDockerBuild(type: Copy, dependsOn: war) { - doFirst { - project.mkdir("$buildDir/docker") - } - from "src/docker" - into "$buildDir/docker" - - from(configurations.docker) - - from(configurations.baseLibs) { - into "libs" - } - - from(sourceSets.test.resources) { - - include "**/conf/context.xml" - eachFile { - path = name - } - includeEmptyDirs = false - } - - from(war.outputs.files.singleFile) { - //include "${project.name}-${project.version}.jar" - into "cda/webapps" - rename(".*\\.war", "cwms-data.war") - } } task generateTimeSeriesSamples(type: JavaExec) { @@ -407,8 +315,6 @@ task generateTimeSeriesSamples(type: JavaExec) { tasks.register("executeOpenAPIConversion", Test) { group 'documentation' - dependsOn generateConfig - dependsOn war useJUnitPlatform() { includeTags "openapi" @@ -419,14 +325,9 @@ tasks.register("executeOpenAPIConversion", Test) { systemProperties += project.properties.findAll { k, v -> k.startsWith("RADAR") && !k.startsWith("RADAR_JDBC") } systemProperties += project.properties.findAll { k, v -> k.startsWith("CDA") && !k.startsWith("CDA_JDBC") } systemProperties += project.properties.findAll { k, v -> k.startsWith("testcontainer") } - jvmArgs += "-DwarFile=$buildDir/libs/${project.name}-${project.version}.war" - jvmArgs += "-DwarContext=/cwms-data" - jvmArgs += "-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" - jvmArgs += "-Djava.util.logging.config.file=$projectDir/logging.properties" - jvmArgs += "-Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource" + jvmArgs += "-Dlogback.configurationFile=${projectDir}/logback.xml" jvmArgs += "-Dcwms.dataapi.access.provider=MultipleAccessManager" jvmArgs += "-Dcwms.dataapi.access.providers=KeyAccessManager,CwmsAccessManager" - jvmArgs += "-Dcatalina.base=$buildDir/tomcat" } tasks.register('shadeCodegenLegacy', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { diff --git a/cwms-data-api/logback-test.xml b/cwms-data-api/logback-test.xml new file mode 100644 index 0000000000..e10ac442d8 --- /dev/null +++ b/cwms-data-api/logback-test.xml @@ -0,0 +1,53 @@ + + + + + + + + + CWMS-Data-API + + + System.err + + ERROR + + + + + + + + + + + false + true + build/cda-test.jsonl + + + + + + + + + + %d [%thread] %-5level - %X{traceId:-none} - %logger - %msg %ex %n + + false + true + build/cda-plain-test.log + + + + + + + + + + + + diff --git a/cwms-data-api/logback.xml b/cwms-data-api/logback.xml index 3771148fb1..c5121d9eba 100644 --- a/cwms-data-api/logback.xml +++ b/cwms-data-api/logback.xml @@ -42,7 +42,9 @@ - + + + diff --git a/cwms-data-api/src/docker/logback.xml b/cwms-data-api/src/docker/logback.xml index 8488573005..aa9a7b209a 100644 --- a/cwms-data-api/src/docker/logback.xml +++ b/cwms-data-api/src/docker/logback.xml @@ -4,7 +4,6 @@ - CWMS-Data-API @@ -12,25 +11,16 @@ System.err - + - + - - - - - - - - - - + \ No newline at end of file diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java deleted file mode 100644 index c339584082..0000000000 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2026 Hydrologic Engineering Center - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package cwms.cda; - -import static cwms.cda.openapi.ExampleUtils.addEndpointExamples; - -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.servlets.MetricsServlet; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.google.common.flogger.FluentLogger; -import cwms.cda.api.Controllers; -import cwms.cda.api.auth.userlists.UserListController; -import cwms.cda.api.enums.UnitSystem; -import cwms.cda.api.errors.ApplicationException; -import cwms.cda.api.errors.CdaError; -import cwms.cda.api.errors.ExceptionTraceSupport; -import cwms.cda.data.dao.JooqDao; -import cwms.cda.data.dao.rss.QueueManager; -import cwms.cda.data.dto.csv.CwmsCsvDTO; -import cwms.cda.formatters.Formats; -import cwms.cda.formatters.csv.CsvExampleGenerator; -import cwms.cda.openapi.OpenApiSchemeProcessor; -import cwms.cda.security.Authenticator; -import cwms.cda.security.CdaAccessManager; -import cwms.cda.security.Role; -import io.github.classgraph.ClassGraph; -import io.github.classgraph.ScanResult; -import io.javalin.Javalin; -import io.javalin.core.JavalinConfig; -import io.javalin.core.security.RouteRole; -import io.javalin.core.util.Header; -import io.javalin.core.validation.JavalinValidation; -import io.javalin.http.BadRequestResponse; -import io.javalin.http.JavalinServlet; -import io.javalin.plugin.openapi.OpenApiOptions; -import io.javalin.plugin.openapi.OpenApiPlugin; -import io.opentelemetry.api.trace.Span; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.responses.ApiResponse; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.servers.Server; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.DateTimeException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.jar.Manifest; -import javax.annotation.Resource; -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.sql.DataSource; -import org.apache.http.entity.ContentType; -import org.jooq.exception.DataAccessException; -import org.owasp.html.HtmlPolicyBuilder; -import org.owasp.html.PolicyFactory; - - -/** - * Setup all the information required so we can serve the request. - * - */ -@WebServlet(urlPatterns = { "/catalog/*", - "/auth/*", - "/swagger-docs", - "/timeseries/*", - "/offices/*", - "/states/*", - "/counties/*", - "/location/*", - "/locations/*", - "/entity/*", - "/parameters/*", - "/timezones/*", - "/units/*", - "/ratings/*", - "/levels/*", - "/level-refs/*", - "/basins/*", - "/streams/*", - "/stream-locations/*", - "/stream-reaches/*", - "/measurements/*", - "/published/*", - "/blobs/*", - "/clobs/*", - "/pools/*", - "/specified-levels/*", - "/forecast-spec/*", - "/forecast-instance/*", - "/standard-text-id/*", - "/projects/*", - "/project-locks/*", - "/project-lock-rights/*", - "/properties/*", - "/lookup-types/*", - "/embankments/*", - "/user/*", - "/users/*", - "/roles/*", - "/version/*", - "/rss/*", - "/v2/*" -}) -public class ApiServlet extends HttpServlet { - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - - // based on https://bitbucket.hecdev.net/projects/CWMS/repos/cwms_aaa/browse/IntegrationTests/src/test/resources/sql/load_testusers.sql - public static final String CWMS_USERS_ROLE = "CWMS Users"; - public static final String CAC_USER = "cac_auth"; - /** Default OFFICE where needed. Based on context. e.g. /cwms-data -> HQ, /spk-data -> SPK */ - public static final String OFFICE_ID = "office_id"; - public static final String DATA_SOURCE = "data_source"; - public static final String RAW_DATA_SOURCE = "data_source"; - public static final String DATABASE = "database"; - public static final String IS_NEW_LRTS = "X-CWMS-LRTS-Formatting"; - - // The VERSION should match the gradle version but not contain the patch version. - // For example 2.4 not 2.4.13 - private static String VERSION; - - public static final String APPLICATION_TITLE = "CWMS Data API"; - public static final String PROVIDER_KEY_OLD = "radar.access.provider"; - public static final String PROVIDER_KEY = "cwms.dataapi.access.provider"; - public static final String DEFAULT_OFFICE_KEY = "cwms.dataapi.default.office"; - public static final String DEFAULT_PROVIDER = "MultipleAccessManager"; - - private MetricRegistry metrics; - private Meter totalRequests; - - private static final long serialVersionUID = 1L; - - JavalinServlet javalin = null; - private final Authenticator authenticator = new Authenticator(); - private final OpenApiSchemeProcessor schemeProcessor = new OpenApiSchemeProcessor(authenticator); - private String appContext; - - @Resource(name = "jdbc/CWMS3") - DataSource cwms; - private CdaAccessManager cdaAccessManager; - - public static String getApiVersion() { - return VERSION != null ? VERSION : "Not Yet Known"; - } - - - @Override - public void destroy() { - javalin.destroy(); - } - - @Override - public void init(ServletConfig config) throws ServletException { - if (VERSION == null) { - ApiServlet.VERSION = obtainFullVersion(config); - } - logger.atInfo().log("Initializing CWMS Data API Version: " + VERSION); - metrics = (MetricRegistry)config.getServletContext() - .getAttribute(MetricsServlet.METRICS_REGISTRY); - totalRequests = metrics.meter("cwms.dataapi.total_requests"); - - super.init(config); - } - - @SuppressWarnings({"java:S125","java:S2095"}) // closed in destroy handler - @Override - public void init() { - logger.atInfo().log("Initializing Javalin."); - JavalinValidation.register(UnitSystem.class, UnitSystem::systemFor); - JavalinValidation.register(JooqDao.DeleteMethod.class, Controllers::getDeleteMethod); - - ObjectMapper om = new ObjectMapper(); - om.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); - om.registerModule(new JavaTimeModule()); - - PolicyFactory sanitizer = new HtmlPolicyBuilder().disallowElements("