diff --git a/.circleci/config.yml b/.circleci/config.yml index c1c9cfe96de..73ca2919e92 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -583,6 +583,7 @@ jobs: -e "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" \ -e "CIRCLE_HONEYBADGER_API_KEY=${CIRCLE_HONEYBADGER_API_KEY}" \ -e "CIRCLE_SHA1=${CIRCLE_SHA1}" \ + -e "CLIENT_STAGE=${CLIENT_STAGE}" \ -e "COGNITO_SUFFIX=${COGNITO_SUFFIX}" \ -e "CURRENT_COLOR=${CURRENT_COLOR}" \ -e "DEPLOYING_COLOR=${DEPLOYING_COLOR}" \ @@ -596,6 +597,7 @@ jobs: -e "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" \ -e "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" \ -e "CIRCLE_SHA1=${CIRCLE_SHA1}" \ + -e "CLIENT_STAGE=${CLIENT_STAGE}" \ -e "COGNITO_SUFFIX=${COGNITO_SUFFIX}" \ -e "CURRENT_COLOR=${CURRENT_COLOR}" \ -e "DEPLOYING_COLOR=${DEPLOYING_COLOR}" \ diff --git a/docs/environments/setup.md b/docs/environments/setup.md index 3bf9efdd2de..b51fd9120cb 100644 --- a/docs/environments/setup.md +++ b/docs/environments/setup.md @@ -88,6 +88,7 @@ A prerequisite for a successful build within CircleCI is [access to CircleCI’s | `IRS_SUPERUSER_EMAIL_PROD` | Email address used to serve all new petitions to the IRS for PROD | | `DEFAULT_ACCOUNT_PASS` | Default password for all test accounts and some password resets | | `STATUSPAGE_DNS_RECORD` | DNS record for Statuspage of CNAME `status.${EFCMS_DOMAIN}` (optional) | + | `CLIENT_STAGE` | The `process.env.STAGE` for the React application | - Run a build in CircleCI. - The build may fail the first time, as provisioning new security certificates can take some time (and cause a timeout). See [the troubleshooting guide](../TROUBLESHOOTING.md) for solutions to common problems. diff --git a/iam/terraform/account-specific/main/lambda-logs-to-elasticsearch.tf b/iam/terraform/account-specific/main/lambda-logs-to-elasticsearch.tf index 28ea1be4d6a..32ec0d8d101 100644 --- a/iam/terraform/account-specific/main/lambda-logs-to-elasticsearch.tf +++ b/iam/terraform/account-specific/main/lambda-logs-to-elasticsearch.tf @@ -111,3 +111,11 @@ resource "aws_cloudwatch_log_subscription_filter" "api_stage_logs_green" { name = "api_stage_logs_lambda_filter" log_group_name = "/aws/apigateway/gateway_api_${element(var.log_group_environments, count.index)}_green" } + +resource "aws_cloudwatch_log_subscription_filter" "legacy_documents_lambda_filter" { + count = length(var.log_group_environments) + destination_arn = aws_lambda_function.logs_to_es.arn + filter_pattern = "" + name = "legacy_documents_${element(var.log_group_environments, count.index)}_lambda_filter" + log_group_name = "/aws/lambda/legacy_documents_migration_lambda_${element(var.log_group_environments, count.index)}" +} diff --git a/shared/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.js b/shared/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.js index 21f119509e8..be246d3c148 100644 --- a/shared/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.js +++ b/shared/src/business/useCases/courtIssuedOrder/fileCourtIssuedOrderInteractor.js @@ -81,6 +81,7 @@ exports.fileCourtIssuedOrderInteractor = async ({ await applicationContext.getPersistenceGateway().saveDocumentFromLambda({ applicationContext, + contentType: 'application/json', document: Buffer.from(JSON.stringify(contentToStore)), key: documentContentsId, useTempBucket: false, diff --git a/shared/src/business/useCases/health/getHealthCheckInteractor.js b/shared/src/business/useCases/health/getHealthCheckInteractor.js index b0118df539c..b995197363e 100644 --- a/shared/src/business/useCases/health/getHealthCheckInteractor.js +++ b/shared/src/business/useCases/health/getHealthCheckInteractor.js @@ -163,12 +163,6 @@ const getEmailServiceStatus = async ({ applicationContext }) => { } }; -const getClamAVStatus = async () => { - // eslint-disable-next-line spellcheck/spell-checker - // TODO - implement once #6282 (Implement ClamAV Fargate Solution) has been completed - return false; -}; - /** * getHealthCheckInteractor * @@ -195,12 +189,7 @@ exports.getHealthCheckInteractor = async ({ applicationContext }) => { applicationContext, }); - const clamAVStatus = await getClamAVStatus({ - applicationContext, - }); - return { - clamAV: clamAVStatus, cognito: cognitoStatus, dynamo: { efcms: dynamoStatus, diff --git a/shared/src/business/useCases/health/getHealthCheckInteractor.test.js b/shared/src/business/useCases/health/getHealthCheckInteractor.test.js index 004277be1d9..df1b571b3d9 100644 --- a/shared/src/business/useCases/health/getHealthCheckInteractor.test.js +++ b/shared/src/business/useCases/health/getHealthCheckInteractor.test.js @@ -40,7 +40,6 @@ describe('getHealthCheckInteractor', () => { }); expect(status).toEqual({ - clamAV: false, cognito: true, dynamo: { efcms: true, @@ -110,7 +109,6 @@ describe('getHealthCheckInteractor', () => { }, }); expect(status).toEqual({ - clamAV: false, cognito: false, dynamo: { efcms: false, diff --git a/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.js b/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.js index 92456df7086..a41c90ea2d3 100644 --- a/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.js +++ b/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.js @@ -1,4 +1,5 @@ const { Case } = require('../../entities/cases/Case'); +const { DocketEntry } = require('../../entities/DocketEntry'); /** * @@ -61,6 +62,7 @@ exports.parseLegacyDocumentsInteractor = async ({ // Save text contents to JSON file in S3 await applicationContext.getPersistenceGateway().saveDocumentFromLambda({ applicationContext, + contentType: 'application/json', document: Buffer.from( JSON.stringify({ documentContents: pdfTextContents }), ), @@ -74,10 +76,17 @@ exports.parseLegacyDocumentsInteractor = async ({ foundDocketEntry.documentContentsId = documentContentsId; + const validatedDocketEntry = new DocketEntry(foundDocketEntry, { + applicationContext, + }).validate(); + await applicationContext.getPersistenceGateway().updateDocketEntry({ applicationContext, docketEntryId: foundDocketEntry.docketEntryId, docketNumber: caseEntity.docketNumber, - document: foundDocketEntry, + document: validatedDocketEntry, }); + applicationContext.logger.info( + `Successfully scraped ${docketNumber}: ${docketEntryId}`, + ); }; diff --git a/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.test.js b/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.test.js index 77a342efc14..19a1f527e42 100644 --- a/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.test.js +++ b/shared/src/business/useCases/migration/parseLegacyDocumentsInteractor.test.js @@ -130,14 +130,12 @@ describe('parseLegacyDocumentsInteractor', () => { docketNumber: mockDocketNumber, }); - expect( - applicationContext.getPersistenceGateway().saveDocumentFromLambda.mock - .calls[0][0].key, - ).toEqual(mockUniqueID); - expect( - applicationContext.getPersistenceGateway().saveDocumentFromLambda.mock - .calls[0][0].document, - ).toEqual( + const saveParams = applicationContext.getPersistenceGateway() + .saveDocumentFromLambda.mock.calls[0][0]; + + expect(saveParams.key).toEqual(mockUniqueID); + expect(saveParams.contentType).toEqual('application/json'); + expect(saveParams.document).toEqual( Buffer.from(JSON.stringify({ documentContents: mockPdfTextContents })), ); }); diff --git a/web-api/terraform/template/lambdas/legacy-documents-migration.js b/web-api/terraform/template/lambdas/legacy-documents-migration.js index 672f189a146..2f34944a8ee 100644 --- a/web-api/terraform/template/lambdas/legacy-documents-migration.js +++ b/web-api/terraform/template/lambdas/legacy-documents-migration.js @@ -11,11 +11,28 @@ exports.handler = async event => { `About to process legacy document for case:${docketNumber}, docketEntryId: ${docketEntryId}`, ); - await applicationContext.getUseCases().parseLegacyDocumentsInteractor({ - applicationContext, - docketEntryId, - docketNumber, - }); + try { + await applicationContext.getUseCases().parseLegacyDocumentsInteractor({ + applicationContext, + docketEntryId, + docketNumber, + }); + } catch (err) { + applicationContext.logger.error( + `Failed processing legacy document ${docketNumber}, ${docketEntryId}: ${err.message}`, + err, + ); + + // try again if error does not have one of the following + if ( + !( + err.message.includes('Docket entry document not found in S3.') || + err.message.includes('Docket entry not found.') + ) + ) { + throw err; + } + } const sqs = applicationContext.getQueueService(); await sqs diff --git a/web-api/terraform/template/legacy-document-migration.tf b/web-api/terraform/template/legacy-document-migration.tf index 9449ae5e72c..0b9d40b6b35 100644 --- a/web-api/terraform/template/legacy-document-migration.tf +++ b/web-api/terraform/template/legacy-document-migration.tf @@ -26,6 +26,7 @@ resource "aws_lambda_function" "legacy_documents_migration_lambda" { DYNAMODB_ENDPOINT = "dynamodb.us-east-1.amazonaws.com" STAGE = var.environment DYNAMODB_TABLE_NAME = var.destination_table + NODE_ENV = "production" } } } diff --git a/web-client/build-dist-public.sh b/web-client/build-dist-public.sh index a3e4bdc4fe2..9d05401a722 100755 --- a/web-client/build-dist-public.sh +++ b/web-client/build-dist-public.sh @@ -17,4 +17,4 @@ CLIENT_ID=$(aws cognito-idp list-user-pool-clients --user-pool-id "${USER_POOL_I COGNITO_LOGIN_URL="https://auth-${ENV}-${COGNITO_SUFFIX}.auth.us-east-1.amazoncognito.com/login?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${COGNITO_REDIRECT_URL}" -COGNITO_LOGIN_URL="${COGNITO_LOGIN_URL}" CIRCLE_SHA1="${CIRCLE_SHA1}" SESSION_TIMEOUT=3300000 API_URL="${API_URL}" npm run build:public +STAGE="${CLIENT_STAGE}" COGNITO_LOGIN_URL="${COGNITO_LOGIN_URL}" CIRCLE_SHA1="${CIRCLE_SHA1}" SESSION_TIMEOUT=3300000 API_URL="${API_URL}" npm run build:public diff --git a/web-client/build-dist.sh b/web-client/build-dist.sh index 1b34cded61f..4e817bf9645 100755 --- a/web-client/build-dist.sh +++ b/web-client/build-dist.sh @@ -7,7 +7,6 @@ DEPLOYING_COLOR=$2 [ -z "${ENV}" ] && echo 'You must pass ENV as argument $1' && exit 1 [ -z "${DEPLOYING_COLOR}" ] && echo 'You must pass DEPLOYING_COLOR as argument $2' && exit 1 - REGION="us-east-1" API_URL="https://api-${DEPLOYING_COLOR}.${EFCMS_DOMAIN}" WS_URL="wss://ws-${DEPLOYING_COLOR}.${EFCMS_DOMAIN}" @@ -27,4 +26,4 @@ else SCANNER_RESOURCE_URI="${DYNAMSOFT_URL_OVERRIDE}/dynamic-web-twain-sdk-14.3.1" fi -CIRCLE_SHA1="${CIRCLE_SHA1}" SESSION_TIMEOUT=3300000 COGNITO_CLIENT_ID="${CLIENT_ID}" SCANNER_RESOURCE_URI="${SCANNER_RESOURCE_URI}" COGNITO_TOKEN_URL="${COGNITO_TOKEN_URL}" COGNITO_REDIRECT_URI="${COGNITO_REDIRECT_URI}" WS_URL="${WS_URL}" API_URL="${API_URL}" COGNITO_LOGIN_URL="${COGNITO_LOGIN_URL}" CIRCLE_HONEYBADGER_API_KEY="${CIRCLE_HONEYBADGER_API_KEY}" npm run build:client +STAGE="${CLIENT_STAGE}" CIRCLE_SHA1="${CIRCLE_SHA1}" SESSION_TIMEOUT=3300000 COGNITO_CLIENT_ID="${CLIENT_ID}" SCANNER_RESOURCE_URI="${SCANNER_RESOURCE_URI}" COGNITO_TOKEN_URL="${COGNITO_TOKEN_URL}" COGNITO_REDIRECT_URI="${COGNITO_REDIRECT_URI}" WS_URL="${WS_URL}" API_URL="${API_URL}" COGNITO_LOGIN_URL="${COGNITO_LOGIN_URL}" CIRCLE_HONEYBADGER_API_KEY="${CIRCLE_HONEYBADGER_API_KEY}" npm run build:client diff --git a/web-client/integration-tests-public/journey/unauthedUserViewsHealthCheck.js b/web-client/integration-tests-public/journey/unauthedUserViewsHealthCheck.js index 2cca5662c4e..93a3d1b3764 100644 --- a/web-client/integration-tests-public/journey/unauthedUserViewsHealthCheck.js +++ b/web-client/integration-tests-public/journey/unauthedUserViewsHealthCheck.js @@ -4,7 +4,6 @@ export const unauthedUserViewsHealthCheck = test => { expect(test.getState('health')).toEqual( expect.objectContaining({ - clamAV: expect.anything(), cognito: expect.anything(), dynamo: expect.objectContaining({ efcms: expect.anything(), diff --git a/web-client/src/presenter/actions/navigateToPractitionerDetailAction.js b/web-client/src/presenter/actions/navigateToPractitionerDetailAction.js index a5043e0fd21..9597aa54967 100644 --- a/web-client/src/presenter/actions/navigateToPractitionerDetailAction.js +++ b/web-client/src/presenter/actions/navigateToPractitionerDetailAction.js @@ -1,13 +1,21 @@ +import { state } from 'cerebral'; + /** * changes the route to the practitioner detail page for the given props.barNumber * * @param {object} providers the providers object + * @param {object} providers.get the cerebral function to retrieve information from state * @param {object} providers.router the riot.router object that is used for changing the route * @param {object} providers.props the cerebral props object * @returns {Promise} async action */ -export const navigateToPractitionerDetailAction = async ({ props, router }) => { - const { barNumber } = props; +export const navigateToPractitionerDetailAction = async ({ + get, + props, + router, +}) => { + const user = get(state.form); + const barNumber = props.barNumber || user.barNumber; if (barNumber) { await router.route(`/practitioner-detail/${barNumber}`); diff --git a/web-client/src/presenter/sequences/adminContactUpdateCompleteSequence.js b/web-client/src/presenter/sequences/adminContactUpdateCompleteSequence.js index 16064ed633f..e1332dd05b4 100644 --- a/web-client/src/presenter/sequences/adminContactUpdateCompleteSequence.js +++ b/web-client/src/presenter/sequences/adminContactUpdateCompleteSequence.js @@ -1,7 +1,13 @@ +import { getUserContactEditCompleteAlertSuccessAction } from '../actions/getUserContactEditCompleteAlertSuccessAction'; +import { setAlertSuccessAction } from '../actions/setAlertSuccessAction'; +import { setSaveAlertsForNavigationAction } from '../actions/setSaveAlertsForNavigationAction'; import { stopWebSocketConnectionAction } from '../actions/webSocketConnection/stopWebSocketConnectionAction'; import { unsetUserContactEditProgressAction } from '../actions/unsetUserContactEditProgressAction'; export const adminContactUpdateCompleteSequence = [ stopWebSocketConnectionAction, unsetUserContactEditProgressAction, + getUserContactEditCompleteAlertSuccessAction, + setAlertSuccessAction, + setSaveAlertsForNavigationAction, ]; diff --git a/web-client/src/presenter/sequences/adminContactUpdateInitialUpdateCompleteSequence.js b/web-client/src/presenter/sequences/adminContactUpdateInitialUpdateCompleteSequence.js index bb02b200e38..f36a38a78eb 100644 --- a/web-client/src/presenter/sequences/adminContactUpdateInitialUpdateCompleteSequence.js +++ b/web-client/src/presenter/sequences/adminContactUpdateInitialUpdateCompleteSequence.js @@ -1,5 +1,7 @@ +import { navigateToPractitionerDetailAction } from '../actions/navigateToPractitionerDetailAction'; import { unsetWaitingForResponseAction } from '../actions/unsetWaitingForResponseAction'; export const adminContactUpdateInitialUpdateCompleteSequence = [ unsetWaitingForResponseAction, + navigateToPractitionerDetailAction, ]; diff --git a/web-client/src/presenter/sequences/submitUpdatePractitionerUserSequence.js b/web-client/src/presenter/sequences/submitUpdatePractitionerUserSequence.js index 99cfd314d45..80a5cc24ba0 100644 --- a/web-client/src/presenter/sequences/submitUpdatePractitionerUserSequence.js +++ b/web-client/src/presenter/sequences/submitUpdatePractitionerUserSequence.js @@ -1,10 +1,6 @@ import { clearAlertsAction } from '../actions/clearAlertsAction'; import { computeFormDateAction } from '../actions/computeFormDateAction'; -import { navigateToPractitionerDetailAction } from '../actions/navigateToPractitionerDetailAction'; -import { setAlertSuccessAction } from '../actions/setAlertSuccessAction'; -import { setCurrentPageAction } from '../actions/setCurrentPageAction'; import { setPractitionerDetailAction } from '../actions/setPractitionerDetailAction'; -import { setSaveAlertsForNavigationAction } from '../actions/setSaveAlertsForNavigationAction'; import { setShowModalFactoryAction } from '../actions/setShowModalFactoryAction'; import { setValidationAlertErrorsAction } from '../actions/setValidationAlertErrorsAction'; import { setValidationErrorsAction } from '../actions/setValidationErrorsAction'; @@ -23,7 +19,6 @@ export const submitUpdatePractitionerUserSequence = [ { error: [setValidationErrorsAction, setValidationAlertErrorsAction], success: [ - setCurrentPageAction('Interstitial'), setWaitingForResponseAction, startWebSocketConnectionAction, { @@ -35,12 +30,7 @@ export const submitUpdatePractitionerUserSequence = [ updatePractitionerUserAction, { error: [], - success: [ - setPractitionerDetailAction, - setSaveAlertsForNavigationAction, - navigateToPractitionerDetailAction, - setAlertSuccessAction, - ], + success: [setPractitionerDetailAction], }, ], }, diff --git a/web-client/terraform/common/cloudfront-edge/header-security-lambda.js b/web-client/terraform/common/cloudfront-edge/header-security-lambda.js index 82c0af1e80b..711631daf50 100644 --- a/web-client/terraform/common/cloudfront-edge/header-security-lambda.js +++ b/web-client/terraform/common/cloudfront-edge/header-security-lambda.js @@ -37,6 +37,7 @@ exports.handler = (event, context, callback) => { const localUrl = 'https://127.0.0.1:*'; const localWebsocketUrl = 'ws://127.0.0.1:*'; const s3Url = 'https://s3.us-east-1.amazonaws.com'; + const statuspageUrl = 'https://lynmjtcq5px1.statuspage.io'; const contentSecurityPolicy = [ 'base-uri resource://pdf.js', `connect-src ${subdomainsUrl} ${applicationUrl} ${cognitoUrl} ${s3Url} ${dynamsoftUrl} ${localUrl} ${websocketUrl} ${localWebsocketUrl} ${honeybadgerApiUrl}`, @@ -44,11 +45,11 @@ exports.handler = (event, context, callback) => { "manifest-src 'self'", `form-action ${applicationUrl} ${subdomainsUrl}`, `object-src ${subdomainsUrl} ${applicationUrl} ${s3Url}`, - `script-src 'self' 'unsafe-inline' ${dynamsoftUrl} resource://pdf.js`, + `script-src 'self' 'unsafe-inline' ${dynamsoftUrl} ${statuspageUrl} resource://pdf.js`, `style-src 'self' 'unsafe-inline' ${dynamsoftUrl}`, `img-src ${applicationUrl} ${subdomainsUrl} data:`, `font-src ${applicationUrl} ${subdomainsUrl}`, - `frame-src ${s3Url} ${subdomainsUrl} blob: data:`, + `frame-src ${s3Url} ${subdomainsUrl} ${statuspageUrl} blob: data:`, "frame-ancestors 'none'", ]; headers['content-security-policy'] = [ diff --git a/web-client/terraform/main/main.tf b/web-client/terraform/main/main.tf index a4de8e221cb..b14024254b1 100644 --- a/web-client/terraform/main/main.tf +++ b/web-client/terraform/main/main.tf @@ -199,3 +199,33 @@ resource "aws_route53_health_check" "ui_health_check" { failure_threshold = "2" request_interval = "30" } + +resource "aws_cloudwatch_metric_alarm" "status_health_check" { + alarm_name = "${var.dns_domain} health check endpoint" + namespace = "AWS/Route53" + metric_name = "HealthCheckStatus" + comparison_operator = "LessThanThreshold" + statistic = "Minimum" + threshold = "1" + evaluation_periods = "2" + period = "60" + + dimensions = { + HealthCheckId = aws_route53_health_check.status_health_check.id + } + + alarm_actions = [data.aws_sns_topic.system_health_alarms.arn] + insufficient_data_actions = [data.aws_sns_topic.system_health_alarms.arn] + ok_actions = [data.aws_sns_topic.system_health_alarms.arn] +} + +resource "aws_route53_health_check" "status_health_check" { + fqdn = "public-api.${var.dns_domain}" + port = 443 + type = "HTTPS_STR_MATCH" + resource_path = "/public-api/health" + failure_threshold = "2" + request_interval = "30" + invert_healthcheck = true + search_string = "false" # Search for any JSON property returning "false" +}