diff --git a/.github/ISSUE_TEMPLATE/up-receiver-migration-validate-prod-data.md b/.github/ISSUE_TEMPLATE/up-receiver-migration-validate-prod-data.md index 7b526162acb..bb12c4e6a60 100644 --- a/.github/ISSUE_TEMPLATE/up-receiver-migration-validate-prod-data.md +++ b/.github/ISSUE_TEMPLATE/up-receiver-migration-validate-prod-data.md @@ -1,7 +1,7 @@ --- -name: UP Migrate Receiver - Move to Production +name: UP Migrate Receiver - Move to Production and Monitor about: This is the third and final step in migrating a STLT to the UP -title: "[name of STLT] - UP Migration - Move to Production" +title: "[name of STLT] - UP Migration - Move to Production and Monitor" labels: onboarding-ops, receiver assignees: '' diff --git a/.github/actions/action-connect-ovpn/.gitignore b/.github/actions/action-connect-ovpn/.gitignore new file mode 100644 index 00000000000..d8a936ebac0 --- /dev/null +++ b/.github/actions/action-connect-ovpn/.gitignore @@ -0,0 +1,4 @@ +.env +*.crt +*.key +*.txt diff --git a/.github/actions/action-connect-ovpn/README.md b/.github/actions/action-connect-ovpn/README.md new file mode 100644 index 00000000000..7f1ab85443f --- /dev/null +++ b/.github/actions/action-connect-ovpn/README.md @@ -0,0 +1,96 @@ +

Actions Connect Open VPN

+ +>*Replaced deprecated [`set-output`](https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/)* + +> v2 switches to openvpn CLI for stability + +## Example file `.ovpn` to connect vpn + +[Example.ovpn](./example.ovpn) + +## Configuration with With + +The following settings must be passed as environment variables as shown in the +example. + +| Key | Value | Suggested Type | Required | Default | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------- | -------- | --------------- | +| `FILE_OVPN` | Location file open vpn and . | `env` | **Yes** | `./config.ovpn` | +| `PING_URL` | URL for check status vpn connect pass or fail | `env` | **Yes** | `127.0.0.1` | +| `SECRET` | Username password for access vpn`(Encode base 64 before set secret.)`[How to encode base 64 ?](https://www.base64encode.org/). | `secret env` | No | `''` | +| `TLS_KEY` | Tls-crypt for access vpn `(Encode base 64 before set secret.)`[How to encode base 64 ?](https://www.base64encode.org/). | `secret env` | No | `''` | + +## Configuration with Env + +The following settings must be passed as environment variables as shown in the +example. + +| Key | Value | Suggested Type | Required | Default | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------- | -------- | ------- | +| `CA_CRT` | Certificate for access vpn `(Encode base 64 before set secret.)`[How to encode base 64 ?](https://www.base64encode.org/). | `secret env` | **Yes** | N/A | +| `USER_CRT` | User certificate for access vpn. `(Encode base 64 before set secret.)`[How to encode base 64 ?](https://www.base64encode.org/). | `secret env` | **Yes** | N/A | +| `USER_KEY` | User key for access vpn. `(Encode base 64 before set secret.)`[How to encode base 64 ?](https://www.base64encode.org/). | `secret env` | **Yes** | N/A | + +## Outputs + +### `STATUS` + +**Boolean** Can get status after connect `true` or `false`. + +## Example usage + +```yml + connect-open-vpn: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install Open VPN + run: sudo apt-get install openvpn + - name: Connect VPN + uses: golfzaptw/action-connect-ovpn@master + id: connect_vpn + with: + PING_URL: '127.0.0.1' + FILE_OVPN: '.github/vpn/config.ovpn' + SECRET: ${{ secrets.SECRET_USERNAME_PASSWORD }} + TLS_KEY: ${{ secrets.TLS_KEY }} + env: + CA_CRT: ${{ secrets.CA_CRT}} + USER_CRT: ${{ secrets.USER_CRT }} + USER_KEY: ${{ secrets.USER_KEY }} + - name: Check Connect VPN + run: echo ${{ steps.connect_vpn.outputs.STATUS }} + - name: kill vpn + if: always() + run: sudo killall openvpn +``` + +## How to prepare file .ovpn + +### Step + +1. Copy the data inside the tags +`` +`` +`` +and encode those values to base64. Then save those values (without a new line!) to the secrets in github actions + +2. In the .ovpn file in your repo, remove the tags +`` +`` +` ` +and replace the values with +``` +ca ca.crt +cert user.crt +key user.key +``` + +This will allow the values to be filled in from Github secrets. + +3. If your open vpn configuration has a username and password please encode those in base64. After that, save the values in the github actions secrets. +format username password +username-vpn +password-vpn + +4. If open vpn have tag `` please repeat step 1 and 2 for the TLS records. diff --git a/.github/actions/action-connect-ovpn/action.yml b/.github/actions/action-connect-ovpn/action.yml new file mode 100644 index 00000000000..1aeac9d9876 --- /dev/null +++ b/.github/actions/action-connect-ovpn/action.yml @@ -0,0 +1,64 @@ +name: 'Connect-VPN-action' +description: 'Connect VPN action' +branding: + icon: 'shield' + color: 'orange' +inputs: + SECRET: + description: 'Username and password for access vpn' + required: false + default: '' + TLS_KEY: + description: 'User key for access vpn' + required: false + default: '' + PING_URL: + description: 'For check success or fail' + required: true + default: '127.0.0.1' + FILE_OVPN: + description: 'Location file open vpn' + required: true + default: './config.ovpn' +outputs: + STATUS: + description: 'Status for check connect vpn' + value: ${{ steps.vpn_status.outputs.vpn-status }} +runs: + using: "composite" + steps: + - name: Install OpenVPN + run: | + sudo apt-get update + sudo apt-get install openvpn + sudo apt-get install openvpn-systemd-resolved + shell: bash + + - name: Connect VPN + env: + TLS_KEY: ${{ inputs.TLS_KEY }} + CA_CRT: ${{ env.CA_CRT}} + USER_CRT: ${{ env.USER_CRT }} + USER_KEY: ${{ env.USER_KEY }} + SECRET: ${{ inputs.SECRET }} + shell: bash + run: | + echo "$TLS_KEY" | base64 -d > tls.key + echo "$CA_CRT" | base64 -d > ca.crt + echo "$USER_CRT" | base64 -d > user.crt + echo "$USER_KEY" | base64 -d > user.key + echo "$SECRET" | base64 -d > secret.txt + sudo openvpn --config ${{ inputs.FILE_OVPN }} --daemon + + - name: VPN Status + id: vpn_status + env: + PING_URL: ${{ inputs.PING_URL }} + shell: bash + run: | + sleep 5 + if ping -c 2 $PING_URL > /dev/null 2>&1; then + echo "vpn-status=true" >> $GITHUB_OUTPUT + else + echo "vpn-status=false" >> $GITHUB_OUTPUT + fi diff --git a/.github/actions/action-connect-ovpn/example.ovpn b/.github/actions/action-connect-ovpn/example.ovpn new file mode 100644 index 00000000000..ee61f15a507 --- /dev/null +++ b/.github/actions/action-connect-ovpn/example.ovpn @@ -0,0 +1,38 @@ +// FULL FILE OVPN + +client +dev tun +proto udp +resolv-retry infinite +nobind +persist-key +persist-tun +remote-cert-tls server +auth-nocache +verb 3 + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + b1:b0:0b:1a:ad:05:54:0f +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIUbPYCDoO+XmScoS84AhQsbnKvd84wCgYIKoZIzj0EAwIw +u1MjifHr6jMxwQ== +-----END CERTIFICATE----- + + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + b1:b0:0b:1a:ad:05:54:0f +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIUbPYCDoO+XmScoS +-----END CERTIFICATE----- + + +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIUbPYCDoO+XmScoS84AhQsbn +-----END CERTIFICATE----- + diff --git a/.github/actions/deploy-backend/action.yml b/.github/actions/deploy-backend/action.yml index a7d23f2bf88..60c558eae6d 100644 --- a/.github/actions/deploy-backend/action.yml +++ b/.github/actions/deploy-backend/action.yml @@ -138,7 +138,6 @@ runs: echo "::add-mask::$value" echo "$secret_get=$value" >> $GITHUB_OUTPUT done - - name: Create ssl key file if: env.USE_DCT == 'true' @@ -152,7 +151,7 @@ runs: shell: bash env: SSL_KEY: ${{ steps.key-vault.outputs[env.KEY_NAME] }} - + - name: Confirm if runner is a signer if: env.USE_DCT == 'true' working-directory: prime-router @@ -333,7 +332,10 @@ runs: - name: Validate function app checksum if: inputs.checksum-validation == 'true' + uses: JosiahSiegel/checksum-validate-action@ebdf8c12c00912d18de93c483b935d51582f9236 + ## DevSecOps - Aquia (Replace) uses: ./.github/actions/checksum-validate-action + with: key: backend validate: true diff --git a/.github/actions/vpn-azure/action.yml b/.github/actions/vpn-azure/action.yml index 803ff5fe6a4..7dd45a24d3e 100644 --- a/.github/actions/vpn-azure/action.yml +++ b/.github/actions/vpn-azure/action.yml @@ -41,6 +41,8 @@ runs: shell: bash - uses: josiahsiegel/action-connect-ovpn@794339aff94452216c97f609476c367a43a31295 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/action-connect-ovpn + if: inputs.env-name && inputs.ca-cert != 'false' id: connect_vpn with: @@ -79,7 +81,7 @@ runs: $env:ARM_CLIENT_SECRET = $servicePrincipal.clientSecret $env:ARM_SUBSCRIPTION_ID = $servicePrincipal.subscriptionId $env:ARM_TENANT_ID = $servicePrincipal.tenantId - + # Save environment variable setup for subsequent steps Get-ChildItem -Path Env: -Recurse -Include ARM_* | ForEach-Object {Write-Output "$($_.Name)=$($_.Value)"} >> $env:GITHUB_ENV shell: pwsh diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8bc1de5d19f..9222d7f6686 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -120,6 +120,11 @@ updates: schedule: interval: "daily" + - package-ecosystem: "github-actions" + directory: "/.github/actions/action-connect-ovpn" + schedule: + interval: "daily" + # Frontend - package-ecosystem: "npm" directory: "/frontend-react" @@ -334,7 +339,7 @@ updates: time: "04:17" timezone: "US/Eastern" rebase-strategy: "disabled" - + # Backend - package-ecosystem: "gradle" directory: "/prime-router" diff --git a/.github/vpn/config.ovpn b/.github/vpn/config.ovpn new file mode 100644 index 00000000000..7a71e54d8c6 --- /dev/null +++ b/.github/vpn/config.ovpn @@ -0,0 +1,21 @@ + +client +dev tun +proto tcp +remote 188.94.28.233 443 +verify-x509-name "C=de, L=Hamburg, O=IT works Consulting GmbH, CN=inf-gw-r1-06, emailAddress=technik@itworks-hh.de" +route remote_host 255.255.255.255 net_gateway +resolv-retry infinite +nobind +persist-key +persist-tun +auth-user-pass secret.txt +cipher AES-256-CBC +auth SHA256 +comp-lzo no +route-delay 4 +verb 3 +reneg-sec 0 +ca ca.crt +cert user.crt +key user.key diff --git a/.github/workflows/alert_terraform_changes.yml b/.github/workflows/alert_terraform_changes.yml index a9033ec99ba..c7730e3a01a 100644 --- a/.github/workflows/alert_terraform_changes.yml +++ b/.github/workflows/alert_terraform_changes.yml @@ -26,9 +26,12 @@ jobs: user-key: ${{ secrets.USER_KEY }} sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} tf-auth: true - + - name: Collect Terraform stats + uses: josiahsiegel/terraform-stats@68b8cbe42c494333fbf6f8d90ac86da1fb69dcc2 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/terraform-stats + id: stats1 with: terraform-directory: operations/app/terraform/vars/${{ matrix.env }} @@ -46,7 +49,7 @@ jobs: echo "resource-drifts=$(echo '${{ steps.stats1.outputs.resource-drifts }}' \ | sed 's/\"/\\\"/g' | sed 's/\\\\\"/\\\\\\"/g')" >> $GITHUB_OUTPUT echo "$EOF" >> $GITHUB_OUTPUT - + - name: Slack Notification if: ${{ steps.format_out.outputs.CHANGES != '' }} uses: ./.github/actions/notifications @@ -57,9 +60,8 @@ jobs: "change-count": "${{ steps.stats1.outputs.change-count }}" "drift-count": "${{ steps.stats1.outputs.drift-count }}" "resource-drifts": "${{ env.resource-drifts }}" - + icon-emoji: ':bell:' channel: pagerduty-alert-dump webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} color: warning - diff --git a/.github/workflows/deploy_terraform.yml b/.github/workflows/deploy_terraform.yml index 82f8ea619f2..a5ce59ce698 100644 --- a/.github/workflows/deploy_terraform.yml +++ b/.github/workflows/deploy_terraform.yml @@ -51,7 +51,10 @@ jobs: sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} tf-auth: true - name: Collect Terraform stats + uses: josiahsiegel/terraform-stats@68b8cbe42c494333fbf6f8d90ac86da1fb69dcc2 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/terraform-stats + id: stats1 with: terraform-directory: operations/app/terraform/vars/${{ needs.pre_job.outputs.env_name }} diff --git a/.github/workflows/log_management.yml b/.github/workflows/log_management.yml index 4da1340e62c..8774b3dadda 100644 --- a/.github/workflows/log_management.yml +++ b/.github/workflows/log_management.yml @@ -12,7 +12,10 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - name: Workflow Housekeeper - workflows NOT in default branch + uses: JosiahSiegel/workflow-housekeeper@731cc20bb613208b34efb6ac74aab4ba147abb50 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/workflow-housekeeper + env: GITHUB_TOKEN: ${{ secrets.LOG_MANAGEMENT_TOKEN }} with: @@ -21,7 +24,10 @@ jobs: retain-run-count: 0 dry-run: false - name: Workflow Housekeeper - workflows in default branch + uses: JosiahSiegel/workflow-housekeeper@731cc20bb613208b34efb6ac74aab4ba147abb50 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/workflow-housekeeper + env: GITHUB_TOKEN: ${{ secrets.LOG_MANAGEMENT_TOKEN }} with: diff --git a/.github/workflows/prepare_deployment_branch.yaml b/.github/workflows/prepare_deployment_branch.yaml index 25cf8aff75e..01c35749047 100644 --- a/.github/workflows/prepare_deployment_branch.yaml +++ b/.github/workflows/prepare_deployment_branch.yaml @@ -27,13 +27,19 @@ jobs: echo "Branch name: \"${BRANCH_NAME}\"" - name: "Create branch '${{ env.BRANCH_NAME }}' to contain the changes for the deployment on ${{ env.DEPLOYMENT_DATE }}" + uses: JosiahSiegel/remote-branch-action@dbe7a2138eb064fbfdb980abee918091a7501fbe + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/remote-branch-action + with: branch: "${{ env.BRANCH_NAME }}" - name: "Prepare a Pull Request from ${{ env.BRANCH_NAME }} into production branch" id: pr + uses: JosiahSiegel/reliable-pull-request-action@ae8d0c88126329ee363a35392793d0bc94cb82e7 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/reliable-pull-request-action + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release_to_azure.yml b/.github/workflows/release_to_azure.yml index 619bc91645a..0c22420eb31 100644 --- a/.github/workflows/release_to_azure.yml +++ b/.github/workflows/release_to_azure.yml @@ -144,7 +144,10 @@ jobs: env: checksum_validation: ${{ vars.CHECKSUM_VALIDATION }} if: needs.pre_job.outputs.has_router_change == 'true' && env.checksum_validation == 'true' + uses: JosiahSiegel/checksum-validate-action@ebdf8c12c00912d18de93c483b935d51582f9236 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/checksum-validate-action + with: key: backend input: $(az functionapp config appsettings list -g prime-data-hub-${{ needs.pre_job.outputs.env_name }} -n pdh${{ needs.pre_job.outputs.env_name }}-functionapp -o tsv | sort) diff --git a/.github/workflows/scan_action_logs.yml b/.github/workflows/scan_action_logs.yml index 2c209176fc4..a618bc92078 100644 --- a/.github/workflows/scan_action_logs.yml +++ b/.github/workflows/scan_action_logs.yml @@ -12,7 +12,10 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - name: Scan run logs + uses: josiahsiegel/runleaks@4dd30d107c03b6ade87978e10c94a77015e488f9 + ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/runleaks + id: scan with: github-token: ${{ secrets.RUNLEAKS_TOKEN }} @@ -24,7 +27,7 @@ jobs: fail-on-leak: false - name: Get scan exceptions if: steps.scan.outputs.count > 0 - run: | + run: | echo "count=${{ steps.scan.outputs.count }}" exceptions='${{ steps.scan.outputs.exceptions }}' exceptions_out=$(echo ${exceptions//"%0A"/} | jq '.') diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 9a8b9d8a923..e6d82c00810 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -21,11 +21,14 @@ dependencies { * Spring WebFlux was chosen for this project to be able to better handle periods of high traffic */ implementation("org.springframework.boot:spring-boot-starter-webflux") - implementation("org.springframework.cloud:spring-cloud-gateway-webflux") + implementation("org.springframework.cloud:spring-cloud-starter-gateway") implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") runtimeOnly("com.nimbusds:oauth2-oidc-sdk:11.19.1") + // Swagger + implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0") + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt index 249ef82f081..756484923fe 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplication.kt @@ -1,9 +1,11 @@ package gov.cdc.prime.reportstream.auth import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication +@ConfigurationPropertiesScan class AuthApplication fun main(args: Array) { diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt index 2c2909dd275..ce68c2e0efc 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt @@ -6,9 +6,16 @@ package gov.cdc.prime.reportstream.auth object AuthApplicationConstants { /** - * All endpoints defined here + * All Auth service endpoints defined here */ object Endpoints { const val HEALTHCHECK_ENDPOINT_V1 = "/api/v1/healthcheck" } + + /** + * All Submissions service endpoints defined here + */ + object SubmissionsEndpoints { + const val REPORTS_ENDPOINT_V1 = "/api/v1/reports" + } } \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt index c0aeb78fdbe..28db94952fc 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt @@ -1,32 +1,21 @@ package gov.cdc.prime.reportstream.auth.config +import gov.cdc.prime.reportstream.auth.model.Environment import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.context.properties.bind.ConstructorBinding import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration import kotlin.time.TimeSource /** * Simple class to automatically read configuration from application.yml (or environment variable overrides) */ -@Configuration -@EnableConfigurationProperties(ProxyConfigurationProperties::class) -class ApplicationConfig( - val proxyConfig: ProxyConfigurationProperties, +@ConfigurationProperties(prefix = "app") +data class ApplicationConfig @ConstructorBinding constructor( + val environment: Environment, ) { @Bean fun timeSource(): TimeSource { return TimeSource.Monotonic } -} - -@ConfigurationProperties("proxy") -data class ProxyConfigurationProperties( - val pathMappings: List, -) - -data class ProxyPathMapping( - val baseUrl: String, - val pathPrefix: String, -) \ No newline at end of file +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/RouteConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/RouteConfig.kt new file mode 100644 index 00000000000..d1aac8c15e6 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/RouteConfig.kt @@ -0,0 +1,27 @@ +package gov.cdc.prime.reportstream.auth.config + +import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +import org.springframework.cloud.gateway.route.RouteLocator +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +/** + * Configuration class to set up route forwarding + */ +@Configuration +class RouteConfig( + private val submissionsConfig: SubmissionsConfig, +) { + + @Bean + fun routes(builder: RouteLocatorBuilder): RouteLocator { + return builder.routes() + .route { + it + .path(AuthApplicationConstants.SubmissionsEndpoints.REPORTS_ENDPOINT_V1) + .uri(submissionsConfig.baseUrl) + } + .build() + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt index 004493a1646..5e3a97f12dc 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SecurityConfig.kt @@ -1,6 +1,8 @@ package gov.cdc.prime.reportstream.auth.config import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +import gov.cdc.prime.reportstream.auth.model.Environment +import org.apache.logging.log4j.kotlin.Logging import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity @@ -14,17 +16,27 @@ import org.springframework.security.web.server.SecurityWebFilterChain */ @Configuration @EnableWebFluxSecurity -class SecurityConfig { +class SecurityConfig( + private val applicationConfig: ApplicationConfig, +) : Logging { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { http + .csrf { it.disable() } // TODO: re-enable after 16312 .authorizeExchange { authorize -> authorize // allow health endpoint without authentication .pathMatchers(AuthApplicationConstants.Endpoints.HEALTHCHECK_ENDPOINT_V1).permitAll() - // all other requests must be authenticated - .anyExchange().authenticated() + + // allow unauthenticated access to swagger on local environments + if (applicationConfig.environment == Environment.LOCAL) { + logger.info("Allowing unauthenticated Swagger access at http://localhost:9000/swagger/ui.html") + authorize.pathMatchers("/swagger/**").permitAll() + } + + // all other requests must be authenticated + authorize.anyExchange().authenticated() } .oauth2ResourceServer { it.opaqueToken { } diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SubmissionsConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SubmissionsConfig.kt new file mode 100644 index 00000000000..703b9a8bccd --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SubmissionsConfig.kt @@ -0,0 +1,12 @@ +package gov.cdc.prime.reportstream.auth.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.bind.ConstructorBinding + +/** + * Configuration for Submissions microservice + */ +@ConfigurationProperties(prefix = "submissions") +data class SubmissionsConfig @ConstructorBinding constructor( + val baseUrl: String, +) \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthController.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthController.kt deleted file mode 100644 index e62df018405..00000000000 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthController.kt +++ /dev/null @@ -1,47 +0,0 @@ -package gov.cdc.prime.reportstream.auth.controller - -import gov.cdc.prime.reportstream.auth.service.ProxyURIStrategy -import kotlinx.coroutines.reactive.awaitSingle -import org.apache.logging.log4j.kotlin.Logging -import org.springframework.cloud.gateway.webflux.ProxyExchange -import org.springframework.http.ResponseEntity -import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.server.ServerWebExchange - -@RestController -class AuthController( - private val proxyURIStrategy: ProxyURIStrategy, -) : Logging { - - /** - * Main workhorse of the application. Handles all incoming requests and properly forwards them given successful - * authentication. Missing or invalid bearer tokens will result in a 401 unauthorized response. - * - * Authentication will be handled by the OAuth 2.0 resource server opaque token configuration - * @see https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/opaque-token.html - * - * Proxying will be handled by the Spring Cloud Gateway library from which the ProxyExchange object is injected - */ - @RequestMapping("**") - suspend fun proxy( - exchange: ServerWebExchange, - proxy: ProxyExchange, - auth: BearerTokenAuthentication, - ): ResponseEntity { - val sub = auth.tokenAttributes["sub"] - val scopes = auth.tokenAttributes["scope"] - - logger.info("Token with sub=$sub and scopes=$scopes is authenticated with Okta") - - val uri = proxyURIStrategy.getTargetURI(exchange.request.uri) - proxy.uri(uri.toString()) - - logger.info("Proxying request to ${exchange.request.method} $uri") - val response = proxy.forward().awaitSingle() - logger.info("Proxy response from ${exchange.request.method} $uri status=${response.statusCode}") - - return response - } -} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/model/Environment.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/model/Environment.kt new file mode 100644 index 00000000000..ec4f1cb9b0c --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/model/Environment.kt @@ -0,0 +1,10 @@ +package gov.cdc.prime.reportstream.auth.model + +/** + * All possible environments the auth app can be running + */ +enum class Environment { + LOCAL, + STAGING, + PRODUCTION, +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/ProxyURIStrategy.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/ProxyURIStrategy.kt deleted file mode 100644 index 38686400a11..00000000000 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/ProxyURIStrategy.kt +++ /dev/null @@ -1,55 +0,0 @@ -package gov.cdc.prime.reportstream.auth.service - -import gov.cdc.prime.reportstream.auth.config.ApplicationConfig -import org.springframework.context.annotation.Profile -import org.springframework.stereotype.Component -import java.net.URI - -/** - * Implementations are ways to decide the ultimate destination of an incoming request - */ -interface ProxyURIStrategy { - fun getTargetURI(incomingUri: URI): URI -} - -/** - * This implementation decides via the path prefix. Currently used locally for when all services are - * running on different ports of localhost. - * - * Configured under proxyConfig.pathMappings - * - * http://localhost:9000/submissions/health -> http://localhost:8880/health - */ -@Component -@Profile("local") -class PathPrefixProxyURIStrategy( - private val applicationConfig: ApplicationConfig, -) : ProxyURIStrategy { - override fun getTargetURI(incomingUri: URI): URI { - val proxyPathMappings = applicationConfig.proxyConfig.pathMappings - val maybePathMapping = proxyPathMappings.find { incomingUri.path.startsWith(it.pathPrefix) } - return if (maybePathMapping != null) { - val baseUri = URI(maybePathMapping.baseUrl) - val path = incomingUri.path.removePrefix(maybePathMapping.pathPrefix) - URI( - baseUri.scheme, - baseUri.userInfo, - baseUri.host, - baseUri.port, - path, - incomingUri.query, - incomingUri.fragment - ) - } else { - throw IllegalStateException("no configured proxy target in path mappings for path=${incomingUri.path}") - } - } -} - -@Component -@Profile("deployed") -class HostProxyPathURIStrategy : ProxyURIStrategy { - override fun getTargetURI(incomingUri: URI): URI { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/auth/src/main/resources/application.yml b/auth/src/main/resources/application.yml index 6a085c848dd..eea5a2c8ec6 100644 --- a/auth/src/main/resources/application.yml +++ b/auth/src/main/resources/application.yml @@ -1,26 +1,28 @@ spring: application: name: "auth" - profiles: - active: local security: oauth2: resourceserver: opaquetoken: # Set client secret in SPRING_SECURITY_OAUTH2_RESOURCESERVER_OPAQUETOKEN_CLIENT_SECRET env variable client-id: 0oaek8tip2lhrhHce1d7 introspection-uri: https://reportstream.oktapreview.com/oauth2/ausekaai7gUuUtHda1d7/v1/introspect - cloud: - gateway: - proxy: - sensitive: [] # pass authorization and cookie headers downstream (filtered by default) server.port: 9000 -proxy.pathMappings: - - pathPrefix: /reportstream - baseUrl: http://localhost:7071 - - pathPrefix: /submissions - baseUrl: http://localhost:8880 +app: + environment: local + +# submissions microservice configuration +submissions: + baseUrl: http://localhost:8080 + +# Ensure these are disabled in production +springdoc: + swagger-ui: + path: /swagger/ui.html + api-docs: + path: /swagger/api-docs #Uncomment for verbose logging #logging: diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthControllerTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthControllerTest.kt deleted file mode 100644 index 8242e7a3780..00000000000 --- a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/AuthControllerTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -package gov.cdc.prime.reportstream.auth.controller - -import gov.cdc.prime.reportstream.auth.service.ProxyURIStrategy -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.kotlin.any -import org.mockito.kotlin.given -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.mock.mockito.MockBean -import org.springframework.http.HttpHeaders -import org.springframework.http.MediaType -import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf -import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOpaqueToken -import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.test.web.reactive.server.WebTestClient -import java.net.URI -import java.nio.charset.Charset -import kotlin.test.Test -import kotlin.test.assertEquals - -@ExtendWith(SpringExtension::class) -@SpringBootTest -@AutoConfigureWebTestClient -class AuthControllerTest @Autowired constructor( - private val webTestClient: WebTestClient, - @MockBean private val mockedUriStrategy: ProxyURIStrategy, -) { - - private val server: MockWebServer = MockWebServer() - - @BeforeEach - fun setUp() { - server.start() - } - - @AfterEach - fun tearDown() { - server.shutdown() - } - - @Test - fun `successful proxy`() { - server.enqueue( - MockResponse() - .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN) - .setBody("hello world!") - ) - - val incomingUri = URI("/service/path") - val outgoingUri = URI(server.url("/path").toString()) - given(mockedUriStrategy.getTargetURI(incomingUri)).willReturn(outgoingUri) - - webTestClient - .mutateWith(csrf()) - .mutateWith( - mockOpaqueToken() - .attributes { map -> - map["sub"] = "sub" - map["scope"] = listOf("scope1", "scope2") - } - ) - .post() - .uri("/service/path") - .accept(MediaType.TEXT_PLAIN) - .headers { headers -> - headers.add("x-test-header", "Pass this along") - } - .bodyValue("body") - .exchange() - // assertions on the response received from the mock server - .expectStatus().isOk - .expectHeader().contentType(MediaType.TEXT_PLAIN) - .expectBody(String::class.java).isEqualTo("hello world!") - - // assertions on recorded request to proxy - val recordedRequest = server.takeRequest() - assertEquals( - recordedRequest.headers.get("x-test-header"), - "Pass this along" - ) - assertEquals( - recordedRequest.body.readString(Charset.defaultCharset()), - "body" - ) - } - - @Test - fun `authorization fails in proxied server`() { - server.enqueue(MockResponse().setResponseCode(403)) - - given(mockedUriStrategy.getTargetURI(any())) - .willReturn(URI(server.url("/").toString())) - - webTestClient - .mutateWith(csrf()) - .mutateWith( - mockOpaqueToken() - .attributes { map -> - map["sub"] = "sub" - map["scope"] = listOf("scope1", "scope2") - } - ) - .post() - .uri("/random") - .accept(MediaType.TEXT_PLAIN) - .headers { headers -> - headers.add("x-test-header", "Pass this along") - } - .bodyValue("body") - .exchange() - // assertions on the response received from the mock server - .expectStatus().isForbidden - - // assertions on recorded request to proxy - val recordedRequest = server.takeRequest() - assertEquals( - recordedRequest.headers.get("x-test-header"), - "Pass this along" - ) - assertEquals( - recordedRequest.body.readString(Charset.defaultCharset()), - "body" - ) - } - - @Test - fun `authentication fails`() { - given(mockedUriStrategy.getTargetURI(any())) - .willReturn(URI(server.url("/").toString())) - - webTestClient - .mutateWith(csrf()) - .post() - .uri("/random") - .exchange() - .expectStatus().isUnauthorized - - // no request should be made to server - assertEquals(server.requestCount, 0) - } -} \ No newline at end of file diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt new file mode 100644 index 00000000000..9a643e15c11 --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt @@ -0,0 +1,29 @@ +package gov.cdc.prime.reportstream.auth.controller + +import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +import gov.cdc.prime.reportstream.auth.model.ApplicationStatus +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.reactive.server.WebTestClient +import kotlin.test.Test + +@ExtendWith(SpringExtension::class) +@SpringBootTest +@AutoConfigureWebTestClient +class HealthControllerTest @Autowired constructor( + private val webTestClient: WebTestClient, +) { + + @Test + fun `successful healthcheck`() { + webTestClient + .get() + .uri(AuthApplicationConstants.Endpoints.HEALTHCHECK_ENDPOINT_V1) + .exchange() + .expectStatus().isOk + .expectBody(ApplicationStatus::class.java) + } +} \ No newline at end of file diff --git a/auth/src/test/resources/application.yml b/auth/src/test/resources/application.yml index 2925d96b9de..0ec6f98f25f 100644 --- a/auth/src/test/resources/application.yml +++ b/auth/src/test/resources/application.yml @@ -10,15 +10,8 @@ spring: client-id: mockClient client-secret: mockSecret introspection-uri: https://localhost:9999/oauth2/default/v1/introspect # should never be hit - cloud: - gateway: - proxy: - sensitive: [] # pass authorization and cookie headers downstream (filtered by default) -server.port: 9000 -proxy.pathMappings: - - pathPrefix: /reportstream - baseUrl: http://localhost:7071 - - pathPrefix: /submissions - baseUrl: http://localhost:8880 +server.port: 9000 +app.environment: local +submissions.baseUrl: http://localhost:8080 \ No newline at end of file diff --git a/frontend-react/.eslintrc.cjs b/frontend-react/.eslintrc.cjs index ab82b5316c1..36139517a94 100644 --- a/frontend-react/.eslintrc.cjs +++ b/frontend-react/.eslintrc.cjs @@ -112,6 +112,7 @@ module.exports = { /* Custom project rules */ "no-console": ["error", { allow: ["warn", "error", "info", "trace"] }], "@typescript-eslint/no-explicit-any": ["off"], + "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "error", { @@ -119,6 +120,8 @@ module.exports = { varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", }, ], "import/order": [ @@ -141,5 +144,6 @@ module.exports = { ], "sort-imports": ["error", { ignoreCase: true, ignoreDeclarationSort: true }], "@typescript-eslint/prefer-nullish-coalescing": ["error"], + "@typescript-eslint/no-empty-object-type": ["error", { allowInterfaces: "always" }], }, }; diff --git a/frontend-react/e2e/helpers/utils.ts b/frontend-react/e2e/helpers/utils.ts index 75570e0cb17..f425d48d1b6 100644 --- a/frontend-react/e2e/helpers/utils.ts +++ b/frontend-react/e2e/helpers/utils.ts @@ -115,7 +115,7 @@ export function fromDateWithTime(date: string, time?: string) { .substring(0, time.length - 2) .split(":") .map(Number); - hours = hours + (time.indexOf("pm") !== -1 ? 12 : 0); + hours = hours + (time.includes("pm") ? 12 : 0); fromDateTime.setHours(hours, minutes, 0, 0); } else { fromDateTime.setHours(0, 0, 0); @@ -132,7 +132,7 @@ export function toDateWithTime(date: string, time?: string) { .substring(0, time.length - 2) .split(":") .map(Number); - hours = hours + (time.indexOf("pm") !== -1 ? 12 : 0); + hours = hours + (time.includes("pm") ? 12 : 0); toDateTime.setHours(hours, minutes, 0, 0); } else { toDateTime.setHours(23, 59, 0); diff --git a/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts b/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts index 34486c7505d..464e238ee77 100644 --- a/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts +++ b/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts @@ -119,7 +119,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = } return { url, status: 200 }; - } catch (error) { + } catch (_error) { warnings.push({ url, message: "Internal link: Page error" }); return { url, status: 400 }; } finally { diff --git a/frontend-react/package.json b/frontend-react/package.json index 4c8c5330e94..a1a5e5ea824 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -5,23 +5,23 @@ "type": "module", "npmClient": "yarn", "dependencies": { - "@microsoft/applicationinsights-react-js": "^17.3.2", - "@microsoft/applicationinsights-web": "^3.3.2", + "@microsoft/applicationinsights-react-js": "^17.3.3", + "@microsoft/applicationinsights-web": "^3.3.3", "@okta/okta-react": "^6.9.0", "@okta/okta-signin-widget": "^7.24.2", "@rest-hooks/rest": "^3.0.3", - "@tanstack/react-query": "^5.55.4", - "@tanstack/react-query-devtools": "^5.55.4", + "@tanstack/react-query": "^5.59.15", + "@tanstack/react-query-devtools": "^5.59.15", "@trussworks/react-uswds": "^9.1.0", "@uswds/uswds": "3.7.1", "axios": "^1.7.7", "classnames": "^2.5.1", "date-fns": "^3.6.0", - "date-fns-tz": "^3.1.3", - "dompurify": "^3.1.6", + "date-fns-tz": "^3.2.0", + "dompurify": "^3.1.7", "downloadjs": "^1.4.7", "export-to-csv-fix-source-map": "^0.2.1", - "focus-trap-react": "^10.2.3", + "focus-trap-react": "^10.3.0", "history": "^5.3.0", "html-to-text": "^9.0.5", "lodash": "^4.17.21", @@ -33,15 +33,15 @@ "react-loader-spinner": "^6.1.6", "react-markdown": "^9.0.1", "react-query-kit": "^3.3.0", - "react-router": "^6.26.1", - "react-router-dom": "^6.26.1", + "react-router": "^6.27.0", + "react-router-dom": "^6.27.0", "react-scroll-sync": "^0.11.2", - "react-toastify": "^10.0.5", + "react-toastify": "^10.0.6", "rehype-raw": "^7.0.0", "rehype-slug": "^5.1.0", "rest-hooks": "^6.1.7", - "sanitize-html": "^2.13.0", - "tsx": "^4.19.0", + "sanitize-html": "^2.13.1", + "tsx": "^4.19.1", "use-deep-compare-effect": "^1.8.1", "uuid": "^10.0.0", "web-vitals": "^3.4.0" @@ -110,25 +110,25 @@ ] }, "devDependencies": { - "@mdx-js/react": "^3.0.1", - "@mdx-js/rollup": "^3.0.1", - "@playwright/test": "^1.47.0", + "@mdx-js/react": "^3.1.0", + "@mdx-js/rollup": "^3.1.0", + "@playwright/test": "^1.48.1", "@rest-hooks/test": "^7.3.1", - "@storybook/addon-a11y": "^8.2.9", - "@storybook/addon-actions": "^8.2.9", - "@storybook/addon-essentials": "^8.2.9", - "@storybook/addon-interactions": "^8.2.9", - "@storybook/addon-links": "^8.2.9", - "@storybook/blocks": "^8.2.9", - "@storybook/components": "^8.2.9", - "@storybook/core-events": "^8.2.9", + "@storybook/addon-a11y": "^8.3.6", + "@storybook/addon-actions": "^8.3.6", + "@storybook/addon-essentials": "^8.3.6", + "@storybook/addon-interactions": "^8.3.6", + "@storybook/addon-links": "^8.3.6", + "@storybook/blocks": "^8.3.6", + "@storybook/components": "^8.3.6", + "@storybook/core-events": "^8.3.6", "@storybook/mdx2-csf": "1.1.0", - "@storybook/react": "^8.2.9", - "@storybook/react-vite": "^8.2.9", + "@storybook/react": "^8.3.6", + "@storybook/react-vite": "^8.3.6", "@storybook/testing-library": "^0.2.2", - "@storybook/theming": "^8.2.9", + "@storybook/theming": "^8.3.6", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/dompurify": "^3.0.5", @@ -136,23 +136,23 @@ "@types/downloadjs": "^1.4.6", "@types/github-slugger": "^1.3.0", "@types/html-to-text": "^9.0.4", - "@types/lodash": "^4.17.7", + "@types/lodash": "^4.17.12", "@types/mdx": "^2.0.13", "@types/node": "^20.12.5", - "@types/react": "18.3.5", - "@types/react-dom": "^18.3.0", + "@types/react": "18.3.11", + "@types/react-dom": "^18.3.1", "@types/react-router-dom": "^5.3.3", "@types/react-scroll-sync": "^0.9.0", "@types/sanitize-html": "^2.13.0", - "@typescript-eslint/eslint-plugin": "^7.17.0", - "@typescript-eslint/parser": "^7.17.0", - "@vitejs/plugin-react": "^4.3.1", - "@vitest/coverage-istanbul": "^2.0.5", - "@vitest/ui": "^2.0.5", + "@typescript-eslint/eslint-plugin": "^8.10.0", + "@typescript-eslint/parser": "^8.10.0", + "@vitejs/plugin-react": "^4.3.3", + "@vitest/coverage-istanbul": "^2.1.3", + "@vitest/ui": "^2.1.3", "autoprefixer": "^10.4.20", - "browserslist": "^4.23.3", + "browserslist": "^4.24.2", "browserslist-useragent-regexp": "^4.1.3", - "chromatic": "^11.7.1", + "chromatic": "^11.12.6", "cross-env": "^7.0.3", "dotenv-flow": "^4.1.0", "eslint": "8.57", @@ -161,43 +161,45 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest-dom": "^5.4.0", "eslint-plugin-jsx-a11y": "^6.9.0", - "eslint-plugin-playwright": "^1.6.2", + "eslint-plugin-playwright": "^1.8.1", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", - "eslint-plugin-storybook": "^0.8.0", - "eslint-plugin-testing-library": "^6.3.0", + "eslint-plugin-storybook": "^0.10.1", + "eslint-plugin-testing-library": "^6.4.0", "eslint-plugin-vitest": "^0.5.4", - "husky": "^9.1.5", - "jsdom": "^25.0.0", + "husky": "^9.1.6", + "jsdom": "^25.0.1", "lint-staged": "^15.2.10", "mockdate": "^3.0.5", - "msw": "^2.3.5", + "msw": "^2.4.11", "msw-storybook-addon": "beta", "npm-run-all": "^4.1.5", - "otpauth": "^9.3.2", + "otpauth": "^9.3.4", "patch-package": "^8.0.0", - "postcss": "^8.4.45", + "postcss": "^8.4.47", "prettier": "^3.3.3", - "react-error-boundary": "^4.0.13", + "react-error-boundary": "^4.1.2", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0", "remark-mdx-toc": "^0.3.1", - "sass": "^1.78.0", - "storybook": "^8.2.9", - "storybook-addon-remix-react-router": "^3.0.0", + "sass": "^1.80.3", + "storybook": "^8.3.6", + "storybook-addon-remix-react-router": "^3.0.1", "ts-node": "^10.9.2", - "tslib": "^2.6.3", - "typescript": "^5.5.4", - "undici": "^6.19.8", - "vite": "^5.4.3", + "tslib": "^2.8.0", + "typescript": "^5.6.3", + "undici": "^6.20.1", + "vite": "^5.4.9", "vite-plugin-checker": "^0.8.0", "vite-plugin-svgr": "^4.2.0", - "vitest": "^2.0.5" + "vitest": "^2.1.3" }, "resolutions": { - "@types/react": "18.3.5", - "@okta/okta-auth-js": ">=7.8.1" + "@types/react": "18.3.11", + "@okta/okta-auth-js": ">=7.8.1", + "cookie": ">=0.7.0", + "send": ">=0.19.0" }, "engines": { "node": "^20.15" diff --git a/frontend-react/src/components/DataDashboard/FacilityProviderSubmitterDetails/FacilityProviderSubmitterSummary.module.scss b/frontend-react/src/components/DataDashboard/FacilityProviderSubmitterDetails/FacilityProviderSubmitterSummary.module.scss index 14a9af71228..8dbe8061bd3 100644 --- a/frontend-react/src/components/DataDashboard/FacilityProviderSubmitterDetails/FacilityProviderSubmitterSummary.module.scss +++ b/frontend-react/src/components/DataDashboard/FacilityProviderSubmitterDetails/FacilityProviderSubmitterSummary.module.scss @@ -1,5 +1,7 @@ +@use "../../../global-modules.scss" as *; + .FacilityProviderSubmitterSummary { - hr { - border: 1px solid color("base-lighter"); - } + hr { + border: 1px solid color("base-lighter"); + } } diff --git a/frontend-react/src/components/FileHandlers/FileHandlerFileUploadStep.test.tsx b/frontend-react/src/components/FileHandlers/FileHandlerFileUploadStep.test.tsx index 56340f66613..cffb8e2846c 100644 --- a/frontend-react/src/components/FileHandlers/FileHandlerFileUploadStep.test.tsx +++ b/frontend-react/src/components/FileHandlers/FileHandlerFileUploadStep.test.tsx @@ -2,14 +2,8 @@ import { fireEvent, screen, waitFor } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { Suspense } from "react"; -import FileHandlerFileUploadStep, { - getClientHeader, -} from "./FileHandlerFileUploadStep"; -import { - fakeFile, - mockSendFileWithErrors, - mockSendValidFile, -} from "../../__mocks__/validation"; +import FileHandlerFileUploadStep, { getClientHeader } from "./FileHandlerFileUploadStep"; +import { fakeFile, mockSendFileWithErrors, mockSendValidFile } from "../../__mocks__/validation"; import { sendersGenerator } from "../../__mockServers__/OrganizationMockServer"; import { RSSender } from "../../config/endpoints/settings"; import { UseSenderResourceHookResult } from "../../hooks/api/organizations/UseOrganizationSender/UseOrganizationSender"; @@ -19,11 +13,7 @@ import useAppInsightsContext from "../../hooks/UseAppInsightsContext/UseAppInsig import { INITIAL_STATE } from "../../hooks/UseFileHandler/UseFileHandler"; import { renderApp } from "../../utils/CustomRenderUtils"; import { MembershipSettings, MemberType } from "../../utils/OrganizationUtils"; -import { - CustomerStatus, - FileType, - Format, -} from "../../utils/TemporarySettingsAPITypes"; +import { CustomerStatus, FileType, Format } from "../../utils/TemporarySettingsAPITypes"; const { mockSessionContentReturnValue } = await vi.importMock< typeof import("../../contexts/Session/__mocks__/useSessionContext") @@ -42,9 +32,7 @@ describe("FileHandlerFileUploadStep", () => { }; const DEFAULT_SENDERS: RSSender[] = sendersGenerator(2); - function mockUseSenderResource( - result: Partial = {}, - ) { + function mockUseSenderResource(result: Partial = {}) { vi.spyOn(useSenderResourceExports, "default").mockReturnValue({ isInitialLoading: false, isLoading: false, @@ -80,14 +68,8 @@ describe("FileHandlerFileUploadStep", () => { test("renders the CSV-specific text", async () => { setup(); - await waitFor(() => - expect(screen.getByText("Upload CSV file")).toBeVisible(), - ); - expect( - screen.getByText( - "Make sure your file has a .csv extension", - ), - ).toBeVisible(); + await waitFor(() => expect(screen.getByText("Upload CSV file")).toBeVisible()); + expect(screen.getByText("Make sure your file has a .csv extension")).toBeVisible(); }); }); @@ -109,16 +91,8 @@ describe("FileHandlerFileUploadStep", () => { test("renders the HL7-specific text", async () => { setup(); - await waitFor(() => - expect( - screen.getByText("Upload HL7 v2.5.1 file"), - ).toBeVisible(), - ); - expect( - screen.getByText( - "Make sure your file has a .hl7 extension", - ), - ).toBeVisible(); + await waitFor(() => expect(screen.getByText("Upload HL7 v2.5.1 file")).toBeVisible()); + expect(screen.getByText("Make sure your file has a .hl7 extension")).toBeVisible(); }); }); @@ -140,20 +114,14 @@ describe("FileHandlerFileUploadStep", () => { ); await waitFor(async () => { - await userEvent.upload( - screen.getByTestId("file-input-input"), - fakeFile, - ); + await userEvent.upload(screen.getByTestId("file-input-input"), fakeFile); await new Promise((res) => setTimeout(res, 100)); }); } test("calls onFileChange with the file and content", async () => { await setup(); - expect(onFileChangeSpy).toHaveBeenCalledWith( - fakeFile, - "foo,bar\r\nbar,foo", - ); + expect(onFileChangeSpy).toHaveBeenCalledWith(fakeFile, "foo,bar\r\nbar,foo"); }); }); @@ -198,8 +166,7 @@ describe("FileHandlerFileUploadStep", () => { vi.spyOn(useWatersUploaderExports, "default").mockReturnValue({ isPending: false, error: null, - mutateAsync: async () => - await Promise.resolve(mockSendValidFile), + mutateAsync: async () => await Promise.resolve(mockSendValidFile), } as any); renderApp( @@ -214,12 +181,7 @@ describe("FileHandlerFileUploadStep", () => { }} fileContent="whatever" fileName="whatever.csv" - file={ - new File( - [new Blob(["whatever"])], - "whatever.csv", - ) - } + file={new File([new Blob(["whatever"])], "whatever.csv")} onFileSubmitSuccess={onFileSubmitSuccessSpy} onNextStepClick={onNextStepClickSpy} /> @@ -234,9 +196,7 @@ describe("FileHandlerFileUploadStep", () => { // eslint-disable-next-line testing-library/no-wait-for-side-effects fireEvent.submit(form); }); - await waitFor(() => - expect(onFileSubmitSuccessSpy).toHaveBeenCalled(), - ); + await waitFor(() => expect(onFileSubmitSuccessSpy).toHaveBeenCalled()); } afterEach(() => { @@ -245,9 +205,7 @@ describe("FileHandlerFileUploadStep", () => { test("it calls onFileSubmitSuccess with the response", async () => { await setup(); - expect(onFileSubmitSuccessSpy).toHaveBeenCalledWith( - mockSendValidFile, - ); + expect(onFileSubmitSuccessSpy).toHaveBeenCalledWith(mockSendValidFile); }); test("it calls onNextStepClick", async () => { @@ -280,6 +238,7 @@ describe("FileHandlerFileUploadStep", () => { isPending: false, error: null, mutateAsync: async () => + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors await Promise.reject({ data: mockSendFileWithErrors, }), @@ -296,12 +255,7 @@ describe("FileHandlerFileUploadStep", () => { }} fileContent="whatever" fileName="whatever.csv" - file={ - new File( - [new Blob(["whatever"])], - "whatever.csv", - ) - } + file={new File([new Blob(["whatever"])], "whatever.csv")} onFileSubmitError={onFileSubmitErrorSpy} /> , @@ -315,9 +269,7 @@ describe("FileHandlerFileUploadStep", () => { // eslint-disable-next-line testing-library/no-wait-for-side-effects fireEvent.submit(form); }); - await waitFor(() => - expect(onFileSubmitErrorSpy).toHaveBeenCalled(), - ); + await waitFor(() => expect(onFileSubmitErrorSpy).toHaveBeenCalled()); } afterEach(() => { @@ -373,47 +325,27 @@ describe("getClientHeader", () => { describe("when selectedSchemaName is falsy", () => { test("returns an empty string", () => { - expect( - getClientHeader( - undefined, - DEFAULT_ACTIVE_MEMBERSHIP, - DEFAULT_SENDER, - ), - ).toEqual(""); + expect(getClientHeader(undefined, DEFAULT_ACTIVE_MEMBERSHIP, DEFAULT_SENDER)).toEqual(""); }); }); describe("when activeMembership is falsy", () => { test("returns an empty string", () => { - expect( - getClientHeader(DEFAULT_SCHEMA_NAME, undefined, DEFAULT_SENDER), - ).toEqual(""); - expect( - getClientHeader(DEFAULT_SCHEMA_NAME, null, DEFAULT_SENDER), - ).toEqual(""); + expect(getClientHeader(DEFAULT_SCHEMA_NAME, undefined, DEFAULT_SENDER)).toEqual(""); + expect(getClientHeader(DEFAULT_SCHEMA_NAME, null, DEFAULT_SENDER)).toEqual(""); }); }); describe("when sender is falsy", () => { test("returns an empty string", () => { - expect( - getClientHeader( - DEFAULT_SCHEMA_NAME, - DEFAULT_ACTIVE_MEMBERSHIP, - undefined, - ), - ).toEqual(""); + expect(getClientHeader(DEFAULT_SCHEMA_NAME, DEFAULT_ACTIVE_MEMBERSHIP, undefined)).toEqual(""); }); }); describe("when activeMembership.parsedName is falsy", () => { test("returns an empty string", () => { expect( - getClientHeader( - DEFAULT_SCHEMA_NAME, - { ...DEFAULT_ACTIVE_MEMBERSHIP, parsedName: "" }, - DEFAULT_SENDER, - ), + getClientHeader(DEFAULT_SCHEMA_NAME, { ...DEFAULT_ACTIVE_MEMBERSHIP, parsedName: "" }, DEFAULT_SENDER), ).toEqual(""); }); }); @@ -421,36 +353,22 @@ describe("getClientHeader", () => { describe("when activeMembership.service is falsy", () => { test("returns an empty string", () => { expect( - getClientHeader( - DEFAULT_SCHEMA_NAME, - { ...DEFAULT_ACTIVE_MEMBERSHIP, service: "" }, - DEFAULT_SENDER, - ), + getClientHeader(DEFAULT_SCHEMA_NAME, { ...DEFAULT_ACTIVE_MEMBERSHIP, service: "" }, DEFAULT_SENDER), ).toEqual(""); }); }); describe("when selected schema value matches sender's schema", () => { test("returns the client value from the organization's parsed name and service", () => { - expect( - getClientHeader( - DEFAULT_SCHEMA_NAME, - DEFAULT_ACTIVE_MEMBERSHIP, - DEFAULT_SENDER, - ), - ).toEqual("orgName.serviceName"); + expect(getClientHeader(DEFAULT_SCHEMA_NAME, DEFAULT_ACTIVE_MEMBERSHIP, DEFAULT_SENDER)).toEqual( + "orgName.serviceName", + ); }); }); describe("when selected schema value does not match the sender's schema", () => { test("returns an empty string", () => { - expect( - getClientHeader( - "bogus-schema", - DEFAULT_ACTIVE_MEMBERSHIP, - DEFAULT_SENDER, - ), - ).toEqual(""); + expect(getClientHeader("bogus-schema", DEFAULT_ACTIVE_MEMBERSHIP, DEFAULT_SENDER)).toEqual(""); }); }); }); diff --git a/frontend-react/src/components/MessageTracker/MessageReceivers.tsx b/frontend-react/src/components/MessageTracker/MessageReceivers.tsx index 72574f51758..d4a8b268dc0 100644 --- a/frontend-react/src/components/MessageTracker/MessageReceivers.tsx +++ b/frontend-react/src/components/MessageTracker/MessageReceivers.tsx @@ -43,17 +43,15 @@ export const ColumnDataTitles = { } as const satisfies { [k in keyof NormalizedReceiverData]: string; }; -export type ColumnDataTitle = - (typeof ColumnDataTitles)[keyof typeof ColumnDataTitles]; +export type ColumnDataTitle = (typeof ColumnDataTitles)[keyof typeof ColumnDataTitles]; export type NormalizedReceiverKey = keyof typeof ColumnDataTitles; -const FilterOptionsEnum = { +export const FilterOptionsEnum = { NONE: "none", ASC: "asc", DESC: "desc", } as const; -export type FilterOption = - (typeof FilterOptionsEnum)[keyof typeof FilterOptionsEnum]; +export type FilterOption = (typeof FilterOptionsEnum)[keyof typeof FilterOptionsEnum]; export const StatusEnum = { BATCH: "batch", @@ -91,22 +89,14 @@ export const MessageReceivers = ({ receiverDetails }: MessageReceiverProps) => { columnKey: "fileLocationMain", columnHeader: "Main", content: (() => { - const status = parseFileLocation( - row?.fileUrl ?? NO_DATA_STRING, - ).folderLocation; + const status = parseFileLocation(row?.fileUrl ?? NO_DATA_STRING).folderLocation; return (

{status.toLocaleUpperCase()}

@@ -116,18 +106,14 @@ export const MessageReceivers = ({ receiverDetails }: MessageReceiverProps) => { { columnKey: "fileLocationSub", columnHeader: "Sub", - content: parseFileLocation(row?.fileUrl ?? NO_DATA_STRING) - .sendingOrg, + content: parseFileLocation(row?.fileUrl ?? NO_DATA_STRING).sendingOrg, }, { columnKey: "fileLocationFileName", columnHeader: "File Name", content: ( - { - parseFileLocation(row?.fileUrl ?? NO_DATA_STRING) - .fileName - } + {parseFileLocation(row?.fileUrl ?? NO_DATA_STRING).fileName} ), }, diff --git a/frontend-react/src/components/USLink.tsx b/frontend-react/src/components/USLink.tsx index 40f22d9c57b..75cde84f143 100644 --- a/frontend-react/src/components/USLink.tsx +++ b/frontend-react/src/components/USLink.tsx @@ -2,12 +2,7 @@ import { IEventTelemetry } from "@microsoft/applicationinsights-web"; import { ButtonProps } from "@trussworks/react-uswds/lib/components/Button/Button"; import classnames from "classnames"; import DOMPurify from "dompurify"; -import { - AnchorHTMLAttributes, - MouseEvent as ReactMouseEvent, - ReactNode, - useMemo, -} from "react"; +import { AnchorHTMLAttributes, MouseEvent as ReactMouseEvent, ReactNode, useMemo } from "react"; import { Link, NavLink, useLocation } from "react-router-dom"; import useAppInsightsContext from "../hooks/UseAppInsightsContext/UseAppInsightsContext"; @@ -20,10 +15,8 @@ interface CustomLinkProps { activeClassName?: string; state?: any; } -type USLinkProps = AnchorHTMLAttributes & - Omit; -type USNavLinkProps = Pick, "href"> & - CustomLinkProps; +type USLinkProps = AnchorHTMLAttributes & Omit; +type USNavLinkProps = Pick, "href"> & CustomLinkProps; /** * Stateless function to get route href from href that could be @@ -39,15 +32,10 @@ export function getHrefRoute(href?: string): string | undefined { if (href === undefined) return undefined; try { - const url = new URL( - href.replace(/^\/\//, `${window.location.protocol}//`), - ); - if ( - url.protocol.startsWith("http") && - url.origin === window.location.origin - ) + const url = new URL(href.replace(/^\/\//, `${window.location.protocol}//`)); + if (url.protocol.startsWith("http") && url.origin === window.location.origin) return `${url.pathname}${url.search}`; - } catch (e: any) { + } catch (_e: any) { return href; } @@ -66,12 +54,7 @@ const sanitizeHref = (href: string | undefined) => { * Sanitizes href and determines if href is an app route or regular * link. */ -export const SafeLink = ({ - children, - href, - state, - ...anchorHTMLAttributes -}: SafeLinkProps) => { +export const SafeLink = ({ children, href, state, ...anchorHTMLAttributes }: SafeLinkProps) => { const sanitizedHref = sanitizeHref(href); const routeHref = getHrefRoute(sanitizedHref); const isFile = sanitizedHref?.startsWith("/assets/"); @@ -102,9 +85,7 @@ export const USLink = ({ children, className, ...props }: USLinkProps) => { ); }; -export interface USLinkButtonProps - extends USLinkProps, - Omit {} +export interface USLinkButtonProps extends USLinkProps, Omit {} export const USLinkButton = ({ className, @@ -131,9 +112,7 @@ export const USLinkButton = ({ className, ); if (isExternalUrl(sanitizeHref(anchorHTMLAttributes.href))) { - return ( - - ); + return ; } return ; }; @@ -151,11 +130,7 @@ export const USLinkButton = ({ * My Site * * */ -export const USExtLink = ({ - className, - children, - ...anchorHTMLAttributes -}: Omit) => { +export const USExtLink = ({ className, children, ...anchorHTMLAttributes }: Omit) => { return ( ( - +export const USCrumbLink = ({ className, children, ...anchorHTMLAttributes }: USLinkProps) => ( + {children} ); @@ -186,13 +154,7 @@ export const USCrumbLink = ({ /** A single link to replace NavLink (react-router-dom). Applies uswds navigation link styling * and handles both active and standard style states. This DOES NOT use `USLink` as a base; it * relies on `NavLink` for additional functionality. */ -export const USNavLink = ({ - href, - children, - className, - activeClassName, - ...props -}: USNavLinkProps) => { +export const USNavLink = ({ href, children, className, activeClassName, ...props }: USNavLinkProps) => { const { hash: currentHash } = useLocation(); const hashIndex = href?.indexOf("#") ?? -1; const hash = hashIndex > -1 ? href?.slice(hashIndex) : ""; @@ -202,8 +164,7 @@ export const USNavLink = ({ to={href ?? ""} className={({ isActive: isPathnameActive }) => { // Without this, all hash links would be considered active for a path - const isActive = - isPathnameActive && (hash === "" || currentHash === hash); + const isActive = isPathnameActive && (hash === "" || currentHash === hash); return classnames("usa-nav__link", { "usa-current": isActive, @@ -227,31 +188,21 @@ export function isExternalUrl(href?: string) { if (href === undefined) return false; try { // Browsers allow // shorthand in anchor urls but URL does not - const url = new URL( - href.replace(/^\/\//, `${window.location.protocol}//`), - ); + const url = new URL(href.replace(/^\/\//, `${window.location.protocol}//`)); return ( - (url.protocol.startsWith("http") && - url.host !== "cdc.gov" && - !url.host.endsWith(".cdc.gov")) || + (url.protocol.startsWith("http") && url.host !== "cdc.gov" && !url.host.endsWith(".cdc.gov")) || href.startsWith("mailto:") ); - } catch (e: any) { + } catch (_e: any) { return false; } } -export interface USSmartLinkProps - extends AnchorHTMLAttributes { +export interface USSmartLinkProps extends AnchorHTMLAttributes { trackClick?: IEventTelemetry; } -export function USSmartLink({ - children, - onClick, - trackClick, - ...props -}: USSmartLinkProps) { +export function USSmartLink({ children, onClick, trackClick, ...props }: USSmartLinkProps) { const appInsights = useAppInsightsContext(); let isExternal = props.href !== undefined; const finalOnClick = useMemo( diff --git a/frontend-react/src/config/endpoints/index.ts b/frontend-react/src/config/endpoints/index.ts index 73a26efb994..f4acce2810f 100644 --- a/frontend-react/src/config/endpoints/index.ts +++ b/frontend-react/src/config/endpoints/index.ts @@ -45,7 +45,7 @@ export class RSEndpoint { } get hasDynamicSegments(): boolean { - return this.path.indexOf("/:") > -1; + return this.path.includes("/:"); } // replaces dynamic paths (`/:` prefixed segments) in an endpoint path @@ -54,30 +54,23 @@ export class RSEndpoint { // would return `/world` toDynamicUrl(segments?: StringIndexed) { if (!segments && this.hasDynamicSegments) { - throw new Error( - `Attempted to use dynamic url without providing segment values: ${this.path}`, - ); + throw new Error(`Attempted to use dynamic url without providing segment values: ${this.path}`); } if (!segments) { return this.url; } const pathWithSegments = Object.entries(segments).reduce( - (pathWithSegments, [segmentKey, segmentValue]) => - pathWithSegments.replace(`:${segmentKey}`, segmentValue), + (pathWithSegments, [segmentKey, segmentValue]) => pathWithSegments.replace(`:${segmentKey}`, segmentValue), this.url, ); - if (pathWithSegments.indexOf("/:") > -1) { - throw new Error( - `missing dynamic path param: ${this.url}, ${JSON.stringify(segments)}`, - ); + if (pathWithSegments.includes("/:")) { + throw new Error(`missing dynamic path param: ${this.url}, ${JSON.stringify(segments)}`); } return pathWithSegments; } // return the complete params that will be passed to axios to make a specific call to this endpoint - toAxiosConfig( - requestOptions: Partial, - ): Partial { + toAxiosConfig(requestOptions: Partial): Partial { const dynamicUrl = this.toDynamicUrl(requestOptions.segments); return { ...omit(requestOptions, "segments"), // this is yucky but necessary for now diff --git a/frontend-react/src/contexts/Session/SessionProvider.tsx b/frontend-react/src/contexts/Session/SessionProvider.tsx index 8ae4dcfca2e..35033e847aa 100644 --- a/frontend-react/src/contexts/Session/SessionProvider.tsx +++ b/frontend-react/src/contexts/Session/SessionProvider.tsx @@ -1,20 +1,7 @@ -import { - AuthState, - CustomUserClaims, - OktaAuth, - UserClaims, -} from "@okta/okta-auth-js"; +import { AuthState, CustomUserClaims, OktaAuth, UserClaims } from "@okta/okta-auth-js"; import { useOktaAuth } from "@okta/okta-react"; import axios, { AxiosError } from "axios"; -import { - createContext, - PropsWithChildren, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { createContext, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { IIdleTimerProps, useIdleTimer } from "react-idle-timer"; import type { AppConfig } from "../../config"; @@ -24,16 +11,8 @@ import useAppInsightsContext from "../../hooks/UseAppInsightsContext/UseAppInsig import { updateApiSessions } from "../../network/Apis"; import { EventName } from "../../utils/AppInsights"; import { isUseragentPreferred } from "../../utils/BrowserUtils"; -import { - MembershipSettings, - membershipsFromToken, - MemberType, - RSUserClaims, -} from "../../utils/OrganizationUtils"; -import { - getUserPermissions, - RSUserPermissions, -} from "../../utils/PermissionsUtils"; +import { MembershipSettings, membershipsFromToken, MemberType, RSUserClaims } from "../../utils/OrganizationUtils"; +import { getUserPermissions, RSUserPermissions } from "../../utils/PermissionsUtils"; import { RSConsole } from "../../utils/rsConsole/rsConsole"; import { RSNetworkError } from "../../utils/RSNetworkError"; @@ -52,10 +31,7 @@ export interface RSSessionContext { config: AppConfig; site: typeof site; rsConsole: RSConsole; - authorizedFetch: ( - options: Partial, - EndpointConfig?: RSEndpoint, - ) => Promise; + authorizedFetch: (options: Partial, EndpointConfig?: RSEndpoint) => Promise; } export const SessionContext = createContext(null as any); @@ -82,12 +58,9 @@ export async function staticAuthorizedFetch({ options, endpointConfig, }: StaticAuthorizedFetchProps) { - if (options.segments && !endpointConfig) - throw new Error("EndpointConfig required when using segments"); - if (options.url && endpointConfig) - throw new Error("Cannot use both url and EndpointConfig"); - if (!options.url && !endpointConfig) - throw new Error("Must use either url or EndpointConfig"); + if (options.segments && !endpointConfig) throw new Error("EndpointConfig required when using segments"); + if (options.url && endpointConfig) throw new Error("Cannot use both url and EndpointConfig"); + if (!options.url && !endpointConfig) throw new Error("Must use either url or EndpointConfig"); const headerOverrides = options?.headers ?? {}; @@ -124,29 +97,16 @@ export async function staticAuthorizedFetch({ } } -function SessionProvider({ - children, - config, - rsConsole, -}: SessionProviderProps) { +function SessionProvider({ children, config, rsConsole }: SessionProviderProps) { const { authState, oktaAuth } = useOktaAuth(); const aiReactPlugin = useAppInsightsContext(); - const initActiveMembership = useRef( - JSON.parse( - sessionStorage.getItem("__deprecatedActiveMembership") ?? "null", - ), - ); - const [_activeMembership, setActiveMembership] = useState( - initActiveMembership.current, - ); + const initActiveMembership = useRef(JSON.parse(sessionStorage.getItem("__deprecatedActiveMembership") ?? "null")); + const [_activeMembership, setActiveMembership] = useState(initActiveMembership.current); const activeMembership = useMemo(() => { - const actualMembership = membershipsFromToken( - authState?.accessToken?.claims, - ); + const actualMembership = membershipsFromToken(authState?.accessToken?.claims); - if (actualMembership == null || !authState?.isAuthenticated) - return undefined; + if (actualMembership == null || !authState?.isAuthenticated) return undefined; return { ...actualMembership, ...(_activeMembership ?? {}) }; }, [authState, _activeMembership]); @@ -161,9 +121,8 @@ function SessionProvider({ } }, [oktaAuth, rsConsole]); - const handleIdle = useCallback< - Exclude - >( + const handleIdle = useCallback>( + // eslint-disable-next-line @typescript-eslint/no-misused-promises async (_event, _timer) => { if (await oktaAuth.isAuthenticated()) { aiReactPlugin.trackEvent({ @@ -186,11 +145,7 @@ function SessionProvider({ const sessionStartTime = useRef(new Date().getTime()); const sessionTimeAggregate = useRef(0); const calculateAggregateTime = () => { - return ( - new Date().getTime() - - sessionStartTime.current + - sessionTimeAggregate.current - ); + return new Date().getTime() - sessionStartTime.current + sessionTimeAggregate.current; }; // do best-attempt window tracking @@ -250,13 +205,10 @@ function SessionProvider({ activeMembership, user: { claims: authState?.idToken?.claims, - ...getUserPermissions( - authState?.accessToken?.claims as RSUserClaims, - ), + ...getUserPermissions(authState?.accessToken?.claims as RSUserClaims), /* This logic is a for when admins have other orgs present on their Okta claims * that interfere with the activeMembership.memberType "soft" check */ - isAdminStrictCheck: - activeMembership?.memberType === MemberType.PRIME_ADMIN, + isAdminStrictCheck: activeMembership?.memberType === MemberType.PRIME_ADMIN, }, logout, _activeMembership, @@ -266,16 +218,7 @@ function SessionProvider({ rsConsole, authorizedFetch, }; - }, [ - oktaAuth, - authState, - activeMembership, - logout, - _activeMembership, - config, - rsConsole, - authorizedFetch, - ]); + }, [oktaAuth, authState, activeMembership, logout, _activeMembership, config, rsConsole, authorizedFetch]); useEffect(() => { updateApiSessions({ @@ -293,10 +236,7 @@ function SessionProvider({ sessionStorage.removeItem("__deprecatedActiveMembership"); sessionStorage.removeItem("__deprecatedFetchInit"); } else { - sessionStorage.setItem( - "__deprecatedActiveMembership", - JSON.stringify(activeMembership), - ); + sessionStorage.setItem("__deprecatedActiveMembership", JSON.stringify(activeMembership)); sessionStorage.setItem( "__deprecatedFetchInit", JSON.stringify({ @@ -315,19 +255,13 @@ function SessionProvider({ // keep auth user up-to-date useEffect(() => { - if ( - authState?.idToken?.claims.email && - !aiReactPlugin.properties.context.user.authenticatedId - ) { + if (authState?.idToken?.claims.email && !aiReactPlugin.properties.context.user.authenticatedId) { aiReactPlugin.properties.context.user.setAuthenticatedUserContext( authState.idToken.claims.email, undefined, true, ); - } else if ( - !authState?.idToken?.claims.email && - aiReactPlugin.properties.context.user.authenticatedId - ) { + } else if (!authState?.idToken?.claims.email && aiReactPlugin.properties.context.user.authenticatedId) { aiReactPlugin.properties.context.user.clearAuthenticatedUserContext(); } }, [authState?.idToken, aiReactPlugin]); @@ -341,11 +275,7 @@ function SessionProvider({ if (!authState) return null; - return ( - - {children} - - ); + return {children}; } export default SessionProvider; diff --git a/frontend-react/src/hooks/UseFileHandler/UseFileHandler.ts b/frontend-react/src/hooks/UseFileHandler/UseFileHandler.ts index 0e86dce3286..86119581012 100644 --- a/frontend-react/src/hooks/UseFileHandler/UseFileHandler.ts +++ b/frontend-react/src/hooks/UseFileHandler/UseFileHandler.ts @@ -66,10 +66,7 @@ export interface FileHandlerAction { payload?: FileHandlerActionPayload; // reset actions will have no payload } -type FileHandlerReducer = ( - state: FileHandlerState, - action: FileHandlerAction, -) => FileHandlerState; +type FileHandlerReducer = (state: FileHandlerState, action: FileHandlerAction) => FileHandlerState; export const INITIAL_STATE = { fileInputResetValue: 0, @@ -109,10 +106,7 @@ function getPreSubmitState(): Partial { } // update state when file is selected in form -function calculateFileSelectedState( - state: FileHandlerState, - payload: FileSelectedPayload, -): Partial { +function calculateFileSelectedState(state: FileHandlerState, payload: FileSelectedPayload): Partial { const { file, fileContent } = payload; let uploadType; if (file.type) { @@ -124,11 +118,7 @@ function calculateFileSelectedState( uploadType = fileNameArray[fileNameArray.length - 1]; } - if ( - uploadType !== "text/csv" && - uploadType !== "csv" && - uploadType !== "hl7" - ) { + if (uploadType !== "text/csv" && uploadType !== "csv" && uploadType !== "hl7") { return { ...state, localError: "The file type must be .csv or .hl7", @@ -145,12 +135,9 @@ function calculateFileSelectedState( // previously loading file contents here // since this is an async action we'll do this in the calling component // prior to dispatching into the reducer, and handle the file content in local state - const contentType = - uploadType === "csv" || uploadType === "text/csv" - ? ContentType.CSV - : ContentType.HL7; + const contentType = uploadType === "csv" || uploadType === "text/csv" ? ContentType.CSV : ContentType.HL7; - const fileType = uploadType.match("hl7") ? FileType.HL7 : FileType.CSV; + const fileType = /hl7/.exec(uploadType) ? FileType.HL7 : FileType.CSV; return { ...state, file, @@ -168,20 +155,10 @@ export function calculateRequestCompleteState( payload: RequestCompletePayload, ): Partial { const { - response: { - destinations, - id, - timestamp, - errors, - status, - warnings, - overallStatus, - }, + response: { destinations, id, timestamp, errors, status, warnings, overallStatus }, } = payload; - const destinationList = destinations?.length - ? destinations.map((d: Destination) => d.organization).join(", ") - : ""; + const destinationList = destinations?.length ? destinations.map((d: Destination) => d.organization).join(", ") : ""; return { destinations: destinationList, @@ -198,10 +175,7 @@ export function calculateRequestCompleteState( }; } -function reducer( - state: FileHandlerState, - action: FileHandlerAction, -): FileHandlerState { +function reducer(state: FileHandlerState, action: FileHandlerAction): FileHandlerState { const { type, payload } = action; switch (type) { case FileHandlerActionType.RESET: @@ -214,17 +188,11 @@ function reducer( return { ...state, ...preSubmitState }; } case FileHandlerActionType.FILE_SELECTED: { - const fileSelectedState = calculateFileSelectedState( - state, - payload as FileSelectedPayload, - ); + const fileSelectedState = calculateFileSelectedState(state, payload as FileSelectedPayload); return { ...state, ...fileSelectedState }; } case FileHandlerActionType.REQUEST_COMPLETE: { - const requestCompleteState = calculateRequestCompleteState( - state, - payload as RequestCompletePayload, - ); + const requestCompleteState = calculateRequestCompleteState(state, payload as RequestCompletePayload); return { ...state, ...requestCompleteState }; } case FileHandlerActionType.SCHEMA_SELECTED: { @@ -262,10 +230,7 @@ export interface UseFileHandlerHookResult { // the pattern laid down in UsePagination for now, in case we need to make this more // complex later - DWS export default function useFileHandler(): UseFileHandlerHookResult { - const [state, dispatch] = useReducer( - reducer, - getInitialState(), - ); + const [state, dispatch] = useReducer(reducer, getInitialState()); /* TODO: possible future refactors: - we could abstract over the dispatch function as UsePagination does and expose individual diff --git a/frontend-react/src/hooks/api/UseReportHistory/UseReportHistory.ts b/frontend-react/src/hooks/api/UseReportHistory/UseReportHistory.ts index 2a63dc47cb7..b550dd8dd8c 100644 --- a/frontend-react/src/hooks/api/UseReportHistory/UseReportHistory.ts +++ b/frontend-react/src/hooks/api/UseReportHistory/UseReportHistory.ts @@ -2,7 +2,6 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { RSOrganizationSettings } from "../../../config/endpoints/settings"; import useSessionContext from "../../../contexts/Session/useSessionContext"; -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface RSReportHistorySearchParams {} export interface RSReportAction { diff --git a/frontend-react/src/pages/admin/AdminRevHistory.test.tsx b/frontend-react/src/pages/admin/AdminRevHistory.test.tsx index 3a45bab4c8e..30fe9cc7d5b 100644 --- a/frontend-react/src/pages/admin/AdminRevHistory.test.tsx +++ b/frontend-react/src/pages/admin/AdminRevHistory.test.tsx @@ -2,10 +2,7 @@ import { screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { _exportForTesting } from "./AdminRevHistory"; -import { - RSSettingRevision, - RSSettingRevisionParams, -} from "../../hooks/api/UseSettingsRevisions/UseSettingsRevisions"; +import { RSSettingRevision, RSSettingRevisionParams } from "../../hooks/api/UseSettingsRevisions/UseSettingsRevisions"; import { renderApp } from "../../utils/CustomRenderUtils"; const fakeRows: RSSettingRevision[] = [ @@ -100,30 +97,24 @@ describe("AdminRevHistory", () => { // make sure the meta data at the bottom is updated. { - const leftMetaText = - screen.getByTestId("meta-left-data").textContent; + const leftMetaText = screen.getByTestId("meta-left-data").textContent; expect(leftMetaText).toBe("Flags: isDeleted: true isActive: false"); } { - const rightMetaText = - screen.getByTestId("meta-right-data").textContent; - expect(rightMetaText).toBe( - "Flags: isDeleted: false isActive: false", - ); + const rightMetaText = screen.getByTestId("meta-right-data").textContent; + expect(rightMetaText).toBe("Flags: isDeleted: false isActive: false"); } // look for the unique "Description" text in each diff. { - const leftDiffText = - screen.getByTestId("left-compare-text").textContent ?? ""; - expect(/ORIGINAL/.test(leftDiffText)).toBe(true); - expect(/FIRST_REVISION/.test(leftDiffText)).toBe(false); + const leftDiffText = screen.getByTestId("left-compare-text").textContent ?? ""; + expect(leftDiffText.includes("ORIGINAL")).toBe(true); + expect(leftDiffText.includes("FIRST_REVISION")).toBe(false); } { - const rightDiffText = - screen.getByTestId("right-compare-text").textContent ?? ""; - expect(/ORIGINAL/.test(rightDiffText)).toBe(false); - expect(/FIRST_REVISION/.test(rightDiffText)).toBe(true); + const rightDiffText = screen.getByTestId("right-compare-text").textContent ?? ""; + expect(rightDiffText.includes("ORIGINAL")).toBe(false); + expect(rightDiffText.includes("FIRST_REVISION")).toBe(true); } }); }); diff --git a/frontend-react/src/pages/misc/FeatureFlags.tsx b/frontend-react/src/pages/misc/FeatureFlags.tsx index 0b34879aba7..4fadebb53bc 100644 --- a/frontend-react/src/pages/misc/FeatureFlags.tsx +++ b/frontend-react/src/pages/misc/FeatureFlags.tsx @@ -1,11 +1,4 @@ -import { - Alert, - Button, - Grid, - GridContainer, - Label, - TextInput, -} from "@trussworks/react-uswds"; +import { Alert, Button, Grid, GridContainer, Label, TextInput } from "@trussworks/react-uswds"; import { useCallback, useRef } from "react"; import { Helmet } from "react-helmet-async"; @@ -40,10 +33,7 @@ export function FeatureFlagsPage() { if (newFlagInputText.current?.value) { newFlagInputText.current.value = ""; } - showToast( - `Feature flag '${newFlag}' added. You will now see UI related to this feature.`, - "success", - ); + showToast(`Feature flag '${newFlag}' added. You will now see UI related to this feature.`, "success"); }, [newFlagInputText, checkFlags, dispatch]); const deleteFlagClick = useCallback( (flagname: string) => { @@ -64,10 +54,7 @@ export function FeatureFlagsPage() {

List of feature flags

-