Skip to content

Commit bd25230

Browse files
Merge pull request #596 from csg-org/development
Sprint 18
2 parents 376e532 + de6100f commit bd25230

File tree

240 files changed

+15046
-4767
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

240 files changed

+15046
-4767
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Compact Connect - Web Frontend Deployment - Production
2+
3+
name: Webroot-Deploy-Production
4+
5+
# Controls when the action will run.
6+
on:
7+
# Triggers the workflow on pushes to trunk branches involving changes to web frontend files
8+
push:
9+
branches:
10+
- main
11+
- frontend/prod-deploy-pipeline
12+
paths:
13+
- webroot/**
14+
15+
# Allows you to run this workflow manually from the Actions tab
16+
workflow_dispatch:
17+
18+
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
19+
jobs:
20+
WebrootDeploy:
21+
# Only run this workflow in certain repos
22+
if: github.repository == 'csg-org/CompactConnect'
23+
24+
# Runner OS
25+
runs-on: ubuntu-latest
26+
27+
# Job needs id-token access to work with GitHub OIDC to AWS IAM Role
28+
permissions:
29+
id-token: write
30+
contents: read
31+
32+
# Define environment-specific values
33+
env:
34+
ENVIRONMENT_NAME: Production Frontend
35+
AWS_REGION: us-east-1
36+
AWS_ROLE: ${{ secrets.PROD_WEBROOT_AWS_ROLE }}
37+
AWS_ROLE_SESSION: WebrootDeployProduction
38+
AWS_S3_BUCKET: ${{ secrets.PROD_WEBROOT_AWS_S3_BUCKET }}
39+
AWS_CLOUDFRONT_DISTRIBUTION: ${{ secrets.PROD_WEBROOT_AWS_CLOUDFRONT_DISTRIBUTION }}
40+
SLACK_BOT_TOKEN: ${{ secrets.IA_SLACK_BOT_TOKEN }}
41+
BASE_URL: /
42+
VUE_APP_DOMAIN: https://app.compactconnect.org
43+
VUE_APP_ROBOTS_META: index,follow
44+
VUE_APP_API_STATE_ROOT: https://api.compactconnect.org
45+
VUE_APP_API_LICENSE_ROOT: https://api.compactconnect.org
46+
VUE_APP_API_USER_ROOT: https://api.compactconnect.org
47+
VUE_APP_COGNITO_REGION: us-east-1
48+
VUE_APP_COGNITO_AUTH_DOMAIN_STAFF: https://compact-connect-staff.auth.us-east-1.amazoncognito.com
49+
VUE_APP_COGNITO_CLIENT_ID_STAFF: ${{ secrets.PROD_WEBROOT_COGNITO_CLIENT_ID_STAFF }}
50+
VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE: https://compact-connect-provider.auth.us-east-1.amazoncognito.com
51+
VUE_APP_COGNITO_CLIENT_ID_LICENSEE: ${{ secrets.PROD_WEBROOT_COGNITO_CLIENT_ID_LICENSEE }}
52+
VUE_APP_RECAPTCHA_KEY: 6LcEQckqAAAAAJUQDEO1KsoeH17-EH5h2UfrwdyK
53+
54+
steps:
55+
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
56+
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
57+
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
58+
59+
# Set AWS CLI credentials
60+
- name: Configure AWS Credentials
61+
uses: aws-actions/configure-aws-credentials@v4
62+
with:
63+
aws-region: ${{ env.AWS_REGION }}
64+
role-to-assume: ${{ env.AWS_ROLE }}
65+
role-session-name: ${{ env.AWS_ROLE_SESSION }}
66+
67+
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
68+
- uses: actions/checkout@v2
69+
70+
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
71+
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
72+
73+
# Setup Node
74+
- name: Setup Node
75+
uses: actions/setup-node@v1
76+
with:
77+
node-version: '22.1.0'
78+
79+
# Use any cached yarn dependencies (saves build time)
80+
- uses: actions/cache@v4
81+
with:
82+
path: '**/node_modules'
83+
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
84+
85+
# Install Yarn Dependencies
86+
- name: Install JS dependencies
87+
run: yarn install --ignore-engines
88+
working-directory: ./webroot
89+
90+
# Run Linter Checks
91+
- name: Run linter
92+
run: yarn lint --no-fix
93+
working-directory: ./webroot
94+
95+
# Build app
96+
- name: Build Vue app
97+
env:
98+
NODE_ENV: production
99+
BASE_URL: ${{ env.BASE_URL }}
100+
VUE_APP_DOMAIN: ${{ env.VUE_APP_DOMAIN }}
101+
VUE_APP_ROBOTS_META: ${{ env.VUE_APP_ROBOTS_META }}
102+
VUE_APP_API_STATE_ROOT: ${{ env.VUE_APP_API_STATE_ROOT }}
103+
VUE_APP_API_LICENSE_ROOT: ${{ env.VUE_APP_API_LICENSE_ROOT }}
104+
VUE_APP_API_USER_ROOT: ${{ env.VUE_APP_API_USER_ROOT }}
105+
VUE_APP_COGNITO_REGION: ${{ env.VUE_APP_COGNITO_REGION }}
106+
VUE_APP_COGNITO_AUTH_DOMAIN_STAFF: ${{ env.VUE_APP_COGNITO_AUTH_DOMAIN_STAFF }}
107+
VUE_APP_COGNITO_CLIENT_ID_STAFF: ${{ env.VUE_APP_COGNITO_CLIENT_ID_STAFF }}
108+
VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE: ${{ env.VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE }}
109+
VUE_APP_COGNITO_CLIENT_ID_LICENSEE: ${{ env.VUE_APP_COGNITO_CLIENT_ID_LICENSEE }}
110+
VUE_APP_RECAPTCHA_KEY: ${{ env.VUE_APP_RECAPTCHA_KEY }}
111+
run: yarn build
112+
working-directory: ./webroot
113+
114+
# Clear out S3 bucket
115+
- name: Clear S3 bucket
116+
run: aws s3 rm ${{ env.AWS_S3_BUCKET }} --recursive
117+
working-directory: ./webroot
118+
119+
# Upload build directory to S3
120+
- name: Upload files to S3
121+
run: aws s3 cp dist ${{ env.AWS_S3_BUCKET }} --recursive
122+
working-directory: ./webroot
123+
124+
# Initiate Cloudfront invalidation
125+
- name: Invalidate cache on Cloudfront distribution
126+
run: >
127+
CLOUDFRONT_INVALIDATION_ID=$(aws cloudfront create-invalidation
128+
--distribution-id ${{ env.AWS_CLOUDFRONT_DISTRIBUTION }}
129+
--paths "/"
130+
--query Invalidation.Id
131+
--output text)
132+
&& echo "CLOUDFRONT_INVALIDATION_ID=$CLOUDFRONT_INVALIDATION_ID" >> $GITHUB_ENV
133+
134+
# Wait for Cloudfront invalidation to complete
135+
- name: Wait for Cloudfront invalidation
136+
run: aws cloudfront wait invalidation-completed --distribution-id ${{ env.AWS_CLOUDFRONT_DISTRIBUTION }} --id ${{ env.CLOUDFRONT_INVALIDATION_ID }}
137+
138+
# Notify to Slack
139+
- name: Post to a Slack channel
140+
uses: slackapi/[email protected]
141+
# https://github.com/slackapi/slack-github-action?tab=readme-ov-file#technique-2-slack-app
142+
with:
143+
channel-id: 'z_jcc_and_inspiringapps'
144+
# https://app.slack.com/block-kit-builder
145+
payload: |
146+
{
147+
"blocks": [
148+
{
149+
"type": "section",
150+
"text": {
151+
"type": "mrkdwn",
152+
"text": "CompactConnect deployment:\n\n*<${{ env.VUE_APP_DOMAIN }}|${{ env.ENVIRONMENT_NAME }} environment>* :rocket:"
153+
}
154+
}
155+
]
156+
}
157+
env:
158+
SLACK_BOT_TOKEN: ${{ env.SLACK_BOT_TOKEN }}

backend/compact-connect/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ unit/functional tests to be incorporated in the development workflow. If you wan
8484
in the cloud, you can do so by configuring context for your own sandbox AWS account with context variables in
8585
`cdk.context.json` and running the appropriate `cdk deploy` command.
8686

87+
Once the deployment completes, you may want to run a local frontend. To do so, you must [populate a `.env`
88+
file](../../webroot/README.md#environment-variables) with data on certain AWS resources (for example, AWS Cognito auth
89+
domains and client IDs). A quick way to do that is to run `bin/fetch_aws_resources.py --as-env` from the
90+
`backend/compact-connect` directory and copy/paste the output into `webroot/.env`. To see more data on your deployment
91+
in human-readable format (for example, DynamoDB table names), run `bin/fetch_aws_resources.py` without any additional
92+
flags.
93+
8794
## Tests
8895
[Back to top](#compact-connect---backend-developer-documentation)
8996

@@ -105,7 +112,7 @@ Keeping documentation current is an important part of feature development in thi
105112
1) Export a fresh api specification (OAS 3.0) is exported from API Gateway and used to update [the Open API Specification JSON file](./docs/api-specification/latest-oas30.json).
106113
2) Run `bin/trim_oas30.py` to organize and trim the API to include only supported API endpoints (and update the script itself, if needed).
107114
3) If you exported the api specification from somewhere other than the CSG Test environment, be sure to set the `servers[0].url` entry back to the correct base URL for the CSG Test environment.
108-
4) Update the [Postman Collection and Environment](./docs/postman) as appropriate.
115+
4) Use `bin/update_postman_collection.py` to update the [Postman Collection and Environment](./docs/postman), based on your new api spec, as appropriate.
109116

110117
## Google reCAPTCHA Setup
111118
[Back to top](#compact-connect---backend-developer-documentation)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
# ruff: noqa: T201 we use print statements for local scripts
3+
"""Script to fetch AWS resources names and IDs required for local deployment. Run from `backend/compact-connect`.
4+
5+
To display in human-readable format:
6+
python fetch_aws_resources.py
7+
8+
To output in .env format:
9+
python fetch_aws_resources.py --as-env
10+
11+
The CLI must also be configured with AWS credentials that have appropriate access to Cognito and DynamoDB
12+
"""
13+
14+
import argparse
15+
16+
import boto3.session
17+
18+
# Initialize AWS clients
19+
cognito_client = boto3.client('cognito-idp')
20+
cloudformation_client = boto3.client('cloudformation')
21+
22+
# Fetch the AWS region
23+
aws_region = boto3.session.Session().region_name
24+
25+
# List of stack names
26+
STACK_NAMES = [
27+
'Sandbox-TransactionMonitoringStack',
28+
'Sandbox-APIStack',
29+
'Sandbox-UIStack',
30+
'Sandbox-IngestStack',
31+
'Sandbox-PersistentStack',
32+
]
33+
34+
35+
def get_stack_outputs(stack_name):
36+
"""Fetch outputs from CloudFormation stack"""
37+
try:
38+
response = cloudformation_client.describe_stacks(StackName=stack_name)
39+
stack = response['Stacks'][0]
40+
return {output['OutputKey']: output['OutputValue'] for output in stack.get('Outputs', [])}
41+
except Exception as e: # noqa: BLE001
42+
print(f'Error retrieving stack {stack_name}: {e}')
43+
return {}
44+
45+
46+
def get_cognito_details(user_pool_id):
47+
"""Fetch Cognito User Pool Name and Client ID"""
48+
try:
49+
pool_response = cognito_client.describe_user_pool(UserPoolId=user_pool_id)
50+
user_pool_name = pool_response['UserPool']['Name']
51+
52+
client_response = cognito_client.list_user_pool_clients(UserPoolId=user_pool_id)
53+
client_id = (
54+
client_response['UserPoolClients'][0]['ClientId']
55+
if client_response['UserPoolClients']
56+
else 'No Client ID Found'
57+
)
58+
59+
return user_pool_name, client_id
60+
except Exception as e: # noqa: BLE001
61+
print(f'Error retrieving Cognito details for {user_pool_id}: {e}')
62+
return 'Unknown', 'Unknown'
63+
64+
65+
def get_cognito_login_url(user_pool_domain):
66+
"""Construct Cognito Hosted UI Login URL"""
67+
if user_pool_domain:
68+
return f'https://{user_pool_domain}.auth.{aws_region}.amazoncognito.com/login'
69+
return None
70+
71+
72+
def extract_table_name(value):
73+
"""Extracts the actual DynamoDB table name from an ARN"""
74+
if value.startswith('arn:aws:dynamodb:'):
75+
return value.split(':')[-1].split('/')[-1]
76+
return value
77+
78+
79+
def fetch_resources():
80+
"""Fetch all required AWS resources"""
81+
api_gateway_url = None
82+
provider_details = {}
83+
staff_details = {}
84+
85+
for stack in STACK_NAMES:
86+
outputs = get_stack_outputs(stack)
87+
88+
for key, value in outputs.items():
89+
# API Gateway Endpoint
90+
if 'ApiGateway' in key or 'Endpoint' in key:
91+
if value.startswith('https://') and 'execute-api' in value:
92+
api_gateway_url = value
93+
94+
# Provider Users (Cognito + DynamoDB)
95+
if 'ProviderUsers' in key:
96+
if 'UserPoolId' in key:
97+
provider_details['user_pool_id'] = value
98+
provider_details['user_pool_name'], provider_details['client_id'] = get_cognito_details(value)
99+
elif 'UsersDomain' in key:
100+
provider_details['login_url'] = get_cognito_login_url(value)
101+
102+
# Staff Users (Cognito + DynamoDB)
103+
if 'StaffUsersGreen' in key:
104+
if 'UserPoolId' in key:
105+
staff_details['user_pool_id'] = value
106+
staff_details['user_pool_name'], staff_details['client_id'] = get_cognito_details(value)
107+
elif 'UsersDomain' in key:
108+
staff_details['login_url'] = get_cognito_login_url(value)
109+
110+
# Find associated DynamoDB tables
111+
if 'Table' in key:
112+
if 'ProviderTable' in value:
113+
provider_details['dynamodb_table'] = extract_table_name(value)
114+
if 'StaffUsersGreen' in value:
115+
staff_details['dynamodb_table'] = extract_table_name(value)
116+
117+
return api_gateway_url, provider_details, staff_details
118+
119+
120+
def print_human_readable(api_gateway_url, provider_details, staff_details):
121+
"""Prints data in a human-readable format"""
122+
print('\n\033[1;34m=== AWS Resource Information ===\033[0m\n') # Blue header
123+
124+
# Print API Gateway URL
125+
if api_gateway_url:
126+
print(f'\033[1;32mAPI Gateway Endpoint:\033[0m {api_gateway_url}\n') # Green header
127+
128+
# Print Provider User Pool Details
129+
print('\033[1;36m=== Provider Users ===\033[0m') # Cyan header
130+
if provider_details:
131+
print(f'\033[1mLogin URL:\033[0m {provider_details.get("login_url", "N/A")}')
132+
print(f'\033[1mCognito User Pool Name:\033[0m {provider_details.get("user_pool_name", "N/A")}')
133+
print(f'\033[1mCognito User Pool ID:\033[0m {provider_details.get("user_pool_id", "N/A")}')
134+
print(f'\033[1mClient ID:\033[0m {provider_details.get("client_id", "N/A")}')
135+
print(f'\033[1mDynamoDB Table:\033[0m {provider_details.get("dynamodb_table", "N/A")}\n')
136+
else:
137+
print('No Provider user pool found.\n')
138+
139+
# Print Staff User Pool Details
140+
print('\033[1;36m=== Staff Users ===\033[0m') # Cyan header
141+
if staff_details:
142+
print(f'\033[1mLogin URL:\033[0m {staff_details.get("login_url", "N/A")}')
143+
print(f'\033[1mCognito User Pool Name:\033[0m {staff_details.get("user_pool_name", "N/A")}')
144+
print(f'\033[1mCognito User Pool ID:\033[0m {staff_details.get("user_pool_id", "N/A")}')
145+
print(f'\033[1mClient ID:\033[0m {staff_details.get("client_id", "N/A")}')
146+
print(f'\033[1mDynamoDB Table:\033[0m {staff_details.get("dynamodb_table", "N/A")}\n')
147+
else:
148+
print('No Staff user pool found.\n')
149+
150+
151+
def print_env_format(api_gateway_url, provider_details, staff_details):
152+
"""Prints data in .env format"""
153+
login_url = provider_details.get('login_url', 'N/A').removesuffix('/login')
154+
staff_client_id = staff_details.get('client_id', 'N/A')
155+
provider_client_id = provider_details.get('client_id', 'N/A')
156+
staff_table = staff_details.get('dynamodb_table', 'N/A')
157+
provider_table = provider_details.get('dynamodb_table', 'N/A')
158+
159+
print(f'VUE_APP_API_STATE_ROOT={api_gateway_url}')
160+
print(f'VUE_APP_API_LICENSE_ROOT={api_gateway_url}')
161+
print(f'VUE_APP_API_USER_ROOT={api_gateway_url}')
162+
print(f'VUE_APP_COGNITO_REGION={aws_region}')
163+
print(f'VUE_APP_COGNITO_AUTH_DOMAIN_STAFF={login_url}')
164+
print(f'VUE_APP_COGNITO_CLIENT_ID_STAFF={staff_client_id}')
165+
print(f'VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE={login_url}')
166+
print(f'VUE_APP_COGNITO_CLIENT_ID_LICENSEE={provider_client_id}')
167+
print(f'VUE_APP_DYNAMO_TABLE_PROVIDER={provider_table}')
168+
print(f'VUE_APP_DYNAMO_TABLE_STAFF={staff_table}')
169+
170+
171+
if __name__ == '__main__':
172+
# Argument parser for --as-env flag
173+
parser = argparse.ArgumentParser(description='Fetch AWS resource details.')
174+
parser.add_argument('--as-env', action='store_true', help='Output in .env format')
175+
args = parser.parse_args()
176+
177+
# Fetch resources
178+
api_gateway_url, provider_details, staff_details = fetch_resources()
179+
180+
# Output in the requested format
181+
if args.as_env:
182+
print_env_format(api_gateway_url, provider_details, staff_details)
183+
else:
184+
print_human_readable(api_gateway_url, provider_details, staff_details)

backend/compact-connect/bin/generate_mock_license_csv_upload_file.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
_context = json.load(context_file)['context']
2323
JURISDICTIONS = _context['jurisdictions']
2424
COMPACTS = _context['compacts']
25-
LICENSE_TYPES = _context['license_types']
25+
LICENSE_TYPES = {compact: [t['name'] for t in types] for compact, types in _context['license_types'].items()}
2626

2727

2828
os.environ['COMPACTS'] = json.dumps(COMPACTS)
@@ -76,7 +76,7 @@ def generate_csv_rows(count, *, compact: str, jurisdiction: str = None) -> dict:
7676
i += 1
7777
if i % 1000 == 0:
7878
sys.stdout.write(f'Generated {i} records')
79-
sys.stdout.write(f'Final record count: {i}')
79+
sys.stdout.write(f'Final record count: {i}\n')
8080

8181

8282
def get_mock_license(i: int, *, compact: str, jurisdiction: str = None) -> dict:

backend/compact-connect/bin/generate_mock_transactions.py

100644100755
File mode changed.

0 commit comments

Comments
 (0)