1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 07:25:19 +02:00

feat: remove AWS dependencies

This commit is contained in:
Lars 2024-01-13 10:09:23 +01:00
parent 879e5d94bc
commit 19e66aa38a
39 changed files with 7 additions and 7226 deletions

View file

@ -1,7 +1,5 @@
# Ignores everything except dist/ prisma/ apps/ and yarn.lock
# apps/ is needed so that CDK (in /aws/maybe-app) has access to the per-app Dockerfile
*
!apps/
!dist/
!prisma/
!yarn.lock

View file

@ -17,11 +17,6 @@ AUTH0_DEPLOY_CLIENT_SECRET=
POSTMARK_SMTP_PASS=
NX_SESSION_SECRET=
# If you want to test any code that utilizes the AWS SDK locally, add temporary STAGING credentials (can be retrieved from SSO dashboard)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=
NX_PLAID_SECRET=
NX_FINICITY_APP_KEY=
NX_FINICITY_PARTNER_SECRET=

2
.gitignore vendored
View file

@ -46,8 +46,6 @@ Thumbs.db
**/.env
**/.env.local
aws/maybe-app/cdk.out
# Next.js
.next

View file

@ -57,8 +57,6 @@ import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicit
import stripe from './stripe'
import postmark from './postmark'
import { managementClient } from './auth0'
import s3 from './s3'
import secretsClient from './secretsClient'
import defineAbilityFor from './ability'
import env from '../../env'
import logger from '../lib/logger'
@ -314,8 +312,6 @@ export async function createContext(req: Request) {
prisma,
plaid,
stripe,
s3,
secretsClient,
managementClient,
logger,
user,

View file

@ -1,77 +0,0 @@
/**
* Custom storage engine for S3
* @see https://github.com/expressjs/multer/blob/master/StorageEngine.md
*
* A simplified version of the `multer-s3` engine that properly supports
* MD5 digests to satisfy S3 Object Lock PutObject requirements
*/
import type { StorageEngine } from 'multer'
import type { Logger } from 'winston'
import { v4 as uuid } from 'uuid'
import mime from 'mime-types'
import crypto from 'crypto'
import type { Request } from 'express'
import type { S3Service } from '@maybe-finance/server/shared'
type S3StorageOpts = {
s3: S3Service
getFilename?: (req: Request) => string
}
class S3Storage implements StorageEngine {
constructor(private readonly opts: S3StorageOpts, private readonly logger: Logger) {
if (!opts.s3) throw new Error('Must provide S3Client instance')
}
_handleFile(...params: Parameters<StorageEngine['_handleFile']>) {
const [req, file, callback] = params
const chunks: any[] = []
file.stream.on('data', (chunk) => chunks.push(chunk))
file.stream.on('end', () => {
const Body = Buffer.concat(chunks)
const md5Hash = crypto.createHash('md5').update(Body).digest('base64')
const filename = this.opts.getFilename ? this.opts.getFilename(req) : null
const Key = `${filename ?? uuid()}.${mime.extension(file.mimetype)}`
this.logger.info(`Multer uploading file key=${Key} size=${Body.length}`)
this.opts.s3
.upload({
bucketKey: 'private',
Key,
Body,
ContentMD5: md5Hash,
})
.then(() => {
callback(null, {
size: Body.length,
path: Key,
mimetype: file.mimetype,
})
})
})
}
_removeFile(...params: Parameters<StorageEngine['_removeFile']>) {
const [req, file, callback] = params
this.logger.warn(`Multer removing file ${file.path}`)
// Since object lock is turned on, this will create a new version, but never delete/overwrite
this.opts.s3
.delete({
bucketKey: 'private',
Key: file.path,
})
.then(() => callback(null))
.catch((err) => callback(err))
}
}
export default function (opts: S3StorageOpts, logger: Logger) {
return new S3Storage(opts, logger) as StorageEngine
}

View file

@ -1,15 +0,0 @@
import { S3Client } from '@aws-sdk/client-s3'
import { S3Service } from '@maybe-finance/server/shared'
import env from '../../env'
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
const s3Client = new S3Client({
region: 'us-west-2',
})
const s3Service = new S3Service(s3Client, {
public: env.NX_CDN_PUBLIC_BUCKET,
private: env.NX_CDN_PRIVATE_BUCKET,
})
export default s3Service

View file

@ -1,7 +0,0 @@
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
const secretsClient = new SecretsManagerClient({
region: 'us-west-2',
})
export default secretsClient

View file

@ -1,15 +0,0 @@
import { S3Client } from '@aws-sdk/client-s3'
import { S3Service } from '@maybe-finance/server/shared'
import env from '../../env'
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
const s3Client = new S3Client({
region: 'us-west-2',
})
const s3Service = new S3Service(s3Client, {
public: env.NX_CDN_PUBLIC_BUCKET,
private: env.NX_CDN_PRIVATE_BUCKET,
})
export default s3Service

View file

@ -1,8 +0,0 @@
*.js
!jest.config.js
*.d.ts
node_modules
# CDK asset staging directory
.cdk.staging
cdk.out

View file

@ -1,6 +0,0 @@
*.ts
!*.d.ts
# CDK asset staging directory
.cdk.staging
cdk.out

View file

@ -1,19 +0,0 @@
## Local deployment
**PROD should not be deployed locally**
To test a local deployment:
1. Set `~/.aws/credentials` file to have a profile called `[maybe_tools]` which should have credentials of IAM user in the tools account (which can cross-deploy to staging/prod accounts). Additional security creds can be generated in AWS dashboard.
```
[maybe_tools]
aws_access_key_id=
aws_secret_access_key=
region=us-east-1
output=json
```
2. Run `yarn cdk:ls` to verify everything is working correctly
3. Run `yarn cdk deploy [SomeStack] --profile=maybe_tools` to deploy a certain stack in the **staging** environment. Production should not be deployed locally ever.

View file

@ -1,93 +0,0 @@
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { getContext } from '../lib/utils/get-context'
import { SharedStack } from '../lib/stacks/shared-stack'
import { ServerStack } from '../lib/stacks/server-stack'
import { WorkersStack } from '../lib/stacks/workers-stack'
import { StackProps } from 'aws-cdk-lib'
import { ToolsStack } from '../lib/stacks/tools-stack'
const app = new cdk.App()
const ctx = getContext(app)
const { AWS_ACCOUNT, DEFAULT_AWS_REGION, ENV_NAME, sharedEnv, stackContexts } = ctx
const commonStackProps: StackProps = {
env: {
region: DEFAULT_AWS_REGION,
account: AWS_ACCOUNT,
},
tags: {
environment: ENV_NAME,
'deploy-type': 'cdk',
},
}
if (ENV_NAME === 'tools') {
new ToolsStack(
app,
'ToolsStack',
{
...commonStackProps,
stackName: `ci-cd-deployments-tools-stack`,
description: 'Resources used for CI/CD, such as self-hosted runners and pipelines',
},
{
DEFAULT_AWS_REGION,
sharedEnv,
...stackContexts.Tools,
}
)
}
if (ENV_NAME === 'staging' || ENV_NAME === 'production') {
const sharedStack = new SharedStack(
app,
'SharedStack',
{
...commonStackProps,
stackName: `maybe-app-${ENV_NAME}-shared-stack`,
description:
'Common infrastructure used by all services including VPC, Redis, and ECS Cluster',
},
{
DEFAULT_AWS_REGION,
sharedEnv,
...stackContexts.Shared,
}
)
new ServerStack(
app,
'ServerStack',
{
...commonStackProps,
stackName: `maybe-app-${ENV_NAME}-server-stack`,
description: 'ECS Fargate RESTful API with ALB, Cloudfront, and WAF',
sharedStack,
},
{
DEFAULT_AWS_REGION,
sharedEnv,
...stackContexts.Server,
}
)
new WorkersStack(
app,
'WorkersStack',
{
...commonStackProps,
stackName: `maybe-app-${ENV_NAME}-workers-stack`,
description: 'Bull.js workers on ECS Fargate',
sharedStack,
},
{
DEFAULT_AWS_REGION,
sharedEnv,
...stackContexts.Workers,
}
)
}

View file

@ -1,20 +0,0 @@
{
"availability-zones:account=180027004049:region=us-west-2": [
"us-west-2a",
"us-west-2b",
"us-west-2c",
"us-west-2d"
],
"availability-zones:account=541001830411:region=us-west-2": [
"us-west-2a",
"us-west-2b",
"us-west-2c",
"us-west-2d"
],
"availability-zones:account=604977304163:region=us-west-2": [
"us-west-2a",
"us-west-2b",
"us-west-2c",
"us-west-2d"
]
}

View file

@ -1,206 +0,0 @@
{
"app": "npx ts-node --prefer-ts-exts bin/maybe-app.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
"@aws-cdk/core:stackRelativeExports": true,
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"environments": {
"tools": {
"ENV_NAME": "tools",
"AWS_ACCOUNT": "REPLACE_THIS",
"DEFAULT_AWS_REGION": "us-west-2",
"sharedEnv": {},
"stackContexts": {
"Tools": {
"VPC": {
"IPAllowList": [""]
},
"GithubRunner": {
"SSMKeyArn": "REPLACE_THIS",
"GithubTokenArn": "REPLACE_THIS"
}
}
}
},
"staging": {
"ENV_NAME": "staging",
"AWS_ACCOUNT": "REPLACE_THIS",
"DEFAULT_AWS_REGION": "us-west-2",
"sharedEnv": {
"NX_API_URL": "https://staging-api.maybe.co",
"NX_CDN_URL": "https://staging-cdn.maybe.co",
"NODE_ENV": "production",
"NX_AUTH0_AUDIENCE": "https://maybe-finance-api/v1",
"NX_PLAID_CLIENT_ID": "REPLACE_THIS",
"NX_PLAID_ENV": "sandbox",
"NX_FINICITY_PARTNER_ID": "REPLACE_THIS",
"NX_FINICITY_ENV": "sandbox",
"NX_CLIENT_URL": "https://staging-app.maybe.co",
"NX_CLIENT_URL_CUSTOM": "https://staging-app.maybe.co",
"NX_AUTH0_DOMAIN": "REPLACE_THIS",
"NX_AUTH0_CUSTOM_DOMAIN": "REPLACE_THIS",
"NX_AUTH0_CLIENT_ID": "REPLACE_THIS",
"NX_AUTH0_MGMT_CLIENT_ID": "REPLACE_THIS",
"NX_SENTRY_ENV": "staging",
"NX_POSTMARK_FROM_ADDRESS": "account@maybe.co"
},
"stackContexts": {
"Shared": {
"PeeringCnxId": "REPLACE_THIS",
"PeeringCnxCidr": "10.1.0.0/16",
"VPC": {
"NATCount": 1,
"IPAllowList": [""]
},
"Redis": {
"size": "cache.t4g.small",
"count": 1
},
"Cloudfront": {
"CertificateArn": "REPLACE_THIS",
"CNAMES": ["staging-cdn.maybe.co"]
},
"UploadOrigins": [
"http://localhost:4200",
"http://localhost:4201",
"https://staging-app.maybe.co"
]
},
"Server": {
"Container": {
"Env": {
"NX_PORT": 3333,
"NX_CORS_ORIGINS": "maybe.co,vercel.app",
"NX_MORGAN_LOG_LEVEL": "combined",
"NX_SENTRY_DSN": "REPLACE_THIS"
}
},
"CertificateArn": "REPLACE_THIS",
"DesiredTaskCount": 1,
"WAFArn": "REPLACE_THIS",
"ComputeCapacity": {
"cpu": 512,
"memory": "1024"
},
"Cloudfront": {
"CNAMES": ["staging-api.maybe.co"],
"CertificateArn": "REPLACE_THIS"
}
},
"Workers": {
"Container": {
"Env": {
"NX_PORT": 3334,
"NX_SENTRY_DSN": "REPLACE_THIS"
}
},
"DesiredTaskCount": 1,
"ComputeCapacity": {
"cpu": 512,
"memory": "1024"
}
}
}
},
"production": {
"ENV_NAME": "production",
"AWS_ACCOUNT": "REPLACE_THIS",
"DEFAULT_AWS_REGION": "us-west-2",
"sharedEnv": {
"NODE_ENV": "production",
"NX_AUTH0_AUDIENCE": "https://maybe-finance-api/v1",
"NX_PLAID_CLIENT_ID": "REPLACE_THIS",
"NX_PLAID_ENV": "production",
"NX_FINICITY_PARTNER_ID": "REPLACE_THIS",
"NX_FINICITY_ENV": "production",
"NX_API_URL": "https://api.maybe.co",
"NX_CDN_URL": "https://cdn.maybe.co",
"NX_CLIENT_URL": "https://app.maybe.co",
"NX_CLIENT_URL_CUSTOM": "https://app.maybe.co",
"NX_AUTH0_DOMAIN": "REPLACE_THIS",
"NX_AUTH0_CUSTOM_DOMAIN": "login.maybe.co",
"NX_AUTH0_CLIENT_ID": "REPLACE_THIS",
"NX_AUTH0_MGMT_CLIENT_ID": "REPLACE_THIS",
"NX_SENTRY_ENV": "production",
"NX_POSTMARK_FROM_ADDRESS": "account@maybe.co",
"NX_STRIPE_PREMIUM_MONTHLY_PRICE_ID": "REPLACE_THIS",
"NX_STRIPE_PREMIUM_YEARLY_PRICE_ID": "REPLACE_THIS"
},
"stackContexts": {
"Shared": {
"PeeringCnxId": "REPLACE_THIS",
"PeeringCnxCidr": "10.2.0.0/16",
"VPC": {
"NATCount": 2,
"IPAllowList": [""]
},
"Redis": {
"size": "cache.m6g.2xlarge",
"count": 1
},
"Cloudfront": {
"CertificateArn": "REPLACE_THIS",
"CNAMES": ["cdn.maybe.co"]
},
"UploadOrigins": "https://app.maybe.co"
},
"Server": {
"Container": {
"Env": {
"NX_PORT": 3333,
"NX_CORS_ORIGINS": "maybe.co",
"NX_MORGAN_LOG_LEVEL": "combined",
"NX_SENTRY_DSN": "REPLACE_THIS"
}
},
"CertificateArn": "REPLACE_THIS",
"DesiredTaskCount": 1,
"WAFArn": "REPLACE_THIS",
"ComputeCapacity": {
"cpu": 4096,
"memory": "8192"
},
"Cloudfront": {
"CNAMES": ["api.maybe.co"],
"CertificateArn": "REPLACE_THIS"
}
},
"Workers": {
"Container": {
"Env": {
"NX_PORT": 3334,
"NX_SENTRY_DSN": "REPLACE_THIS"
}
},
"DesiredTaskCount": 1,
"ComputeCapacity": {
"cpu": 4096,
"memory": "8192"
}
}
}
}
}
}
}

View file

@ -1,8 +0,0 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
}

View file

@ -1,73 +0,0 @@
#!/bin/bash
# Go to home directory
cd /home/ubuntu
# Install necessary packages
apt-get update
apt-get install unzip jq -y
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
export PATH="$(which aws):$PATH"
source ~/.bashrc
aws --version
# Initializes the Github runner
export RUNNER_ALLOW_RUNASROOT="1" # This allows the root user to set everything up
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.291.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.291.1/actions-runner-linux-x64-2.291.1.tar.gz
echo "1bde3f2baf514adda5f8cf2ce531edd2f6be52ed84b9b6733bf43006d36dcd4c actions-runner-linux-x64-2.291.1.tar.gz" | shasum -a 256 -c
tar xzf ./actions-runner-linux-x64-2.291.1.tar.gz
# Grabs Github API key from Secure SSM param
# Make sure there is a secure parameter called '/github/api-token' in format githubusername:githubpersonalaccesstoken (with "repo" scopes enabled)
GITHUB_API_TOKEN=$(aws ssm get-parameter --name /github/api-token --region us-west-2 --with-decryption | jq -r '.Parameter.Value')
# Uses token to get a runner registration token, which is used to configure the runner
ADD_RUNNER_TOKEN=$(curl -u $GITHUB_API_TOKEN \
-X POST \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/maybe-finance/maybe-app/actions/runners/registration-token | jq -r '.token')
./config.sh --url https://github.com/maybe-finance/maybe-app --labels aws --unattended --token $ADD_RUNNER_TOKEN
./svc.sh install
# Install Node v.16.15 and yarn
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install v16.15.0
nvm use v16.15.0
nvm alias default v16.15.0
npm install -g yarn
# Need to add these binaries to the runner's `.path` file, which is what the runner uses to locate packages
# https://github.com/actions/setup-node/issues/182#issuecomment-718233039
echo $PATH > .path
echo -n ":/home/ubuntu/.nvm/versions/node/v16.15.0/bin" >> .path
# Install Docker
apt-get update
apt-get install \
ca-certificates \
curl \
gnupg \
lsb-release -y
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
# https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
# Give non-root user ability to run docker commands
usermod -aG docker ubuntu
newgrp docker
# Start the Github runner as a service
sudo ./svc.sh start

View file

@ -1,104 +0,0 @@
import {
BlockDeviceVolume,
Instance,
InstanceClass,
InstanceSize,
InstanceType,
IVpc,
MachineImage,
Peer,
Port,
SecurityGroup,
SubnetType,
} from 'aws-cdk-lib/aws-ec2'
import {
ManagedPolicy,
PolicyDocument,
PolicyStatement,
Role,
ServicePrincipal,
} from 'aws-cdk-lib/aws-iam'
import { Construct } from 'constructs'
import { ToolsStackContext } from '../../utils/get-context'
import * as path from 'path'
import { readFileSync } from 'fs'
export interface GithubActionsRunnerProps {
vpc: IVpc
}
/**
* Registers an on-demand EC2 instance for running GH Actions workflows
*/
export class GithubActionsRunner extends Construct {
constructor(
scope: Construct,
id: string,
props: GithubActionsRunnerProps,
ctx: ToolsStackContext
) {
super(scope, id)
const runnerSG = new SecurityGroup(this, 'RunnerSecurityGroup', {
vpc: props.vpc,
})
ctx.VPC.IPAllowList.forEach((ip, index) => {
runnerSG.addIngressRule(Peer.ipv4(`${ip}/32`), Port.tcp(22))
})
// Details retrieved from console, see command below:
// aws ec2 describe-images --region us-west-2 --image-ids ami-0cfa91bdbc3be780c
const ami = {
id: 'ami-0cfa91bdbc3be780c',
deviceName: '/dev/sda1',
}
const runner = new Instance(this, 'GithubActionsRunner', {
vpc: props.vpc,
vpcSubnets: { subnetType: SubnetType.PUBLIC },
machineImage: MachineImage.genericLinux({
[ctx.DEFAULT_AWS_REGION]: ami.id,
}),
instanceType: InstanceType.of(InstanceClass.T3A, InstanceSize.MEDIUM),
securityGroup: runnerSG,
// This role allows for Session Manager connections - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html
role: new Role(this, 'SessionManagerRole', {
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
description: 'Allows Session Manager to connect to EC2 instance',
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
inlinePolicies: {
// Allows EC2 instance to grab the Github PAT and make a call to get a "create runner" registration token from Github API
ReadGithubKey: new PolicyDocument({
statements: [
new PolicyStatement({
actions: ['kms:Decrypt'],
resources: [ctx.GithubRunner.SSMKeyArn],
}),
new PolicyStatement({
actions: ['ssm:GetParameters'],
resources: [ctx.GithubRunner.GithubTokenArn],
}),
],
}),
},
}),
blockDevices: [
{
deviceName: ami.deviceName,
volume: BlockDeviceVolume.ebs(100),
},
],
userDataCausesReplacement: true,
})
runner.addUserData(
readFileSync(path.join(__dirname, './configure-github-runner.sh'), 'utf8')
)
// Associate the keypair for SSH connections - https://stackoverflow.com/a/60713522/7437737
runner.instance.addPropertyOverride('KeyName', 'gh-actions-runner-key') // key was manually created in Console, available in 1Password
}
}

View file

@ -1,60 +0,0 @@
import {
Instance,
InstanceClass,
InstanceSize,
InstanceType,
IVpc,
MachineImage,
Peer,
Port,
SecurityGroup,
SubnetType,
} from 'aws-cdk-lib/aws-ec2'
import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'
import { Construct } from 'constructs'
import { SharedStackContext } from '../utils/get-context'
export interface JumpBoxProps {
vpc: IVpc
}
/**
* Creates a "Jump Box", or "Bastion" EC2 instance in public subnet for connections to Postgres
*/
export class JumpBox extends Construct {
public readonly redisUrl: string
constructor(scope: Construct, id: string, props: JumpBoxProps, ctx: SharedStackContext) {
super(scope, id)
const jumpBoxSecurityGroup = new SecurityGroup(this, 'JumpBoxSecurityGroup', {
vpc: props.vpc,
})
// Allow SSH connections to specified IP addresses
ctx.VPC.IPAllowList.forEach((ip, index) => {
jumpBoxSecurityGroup.addIngressRule(Peer.ipv4(`${ip}/32`), Port.tcp(22))
})
const jumpBox = new Instance(this, 'JumpBox', {
vpc: props.vpc,
vpcSubnets: { subnetType: SubnetType.PUBLIC },
machineImage: MachineImage.genericLinux({
[ctx.DEFAULT_AWS_REGION]: 'ami-0cfa91bdbc3be780c',
}),
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO),
securityGroup: jumpBoxSecurityGroup,
// This role allows for Session Manager connections - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html
role: new Role(this, 'SessionManagerRole', {
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
description: 'Allows Session Manager to connect to EC2 instance',
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
}),
})
// Associate the keypair for SSH connections - https://stackoverflow.com/a/60713522/7437737
jumpBox.instance.addPropertyOverride('KeyName', 'jumpbox-key') // key was manually created in Console, available in 1Password
}
}

View file

@ -1,130 +0,0 @@
import {
AclCidr,
AclTraffic,
Action,
IVpc,
NetworkAcl,
SubnetType,
TrafficDirection,
} from 'aws-cdk-lib/aws-ec2'
import { Construct } from 'constructs'
export interface NetworkACLConfigProps {
vpc: IVpc
authorizedIPs?: string[]
}
/**
* Creates the "best practice" network ACL config for a public/private subnet VPC (some rules are more lenient than the docs suggest)
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html#nacl-rules-scenario-2
*/
export class NetworkACLConfig extends Construct {
public readonly redisUrl: string
constructor(scope: Construct, id: string, props: NetworkACLConfigProps) {
super(scope, id)
const publicNetworkACL = new NetworkAcl(this, 'PublicNetworkACL', {
vpc: props.vpc,
subnetSelection: { subnetType: SubnetType.PUBLIC },
})
// --------------------------------
// Inbound Public Entries
// --------------------------------
publicNetworkACL.addEntry('InboundHTTPPublic', {
cidr: AclCidr.anyIpv4(),
ruleNumber: 101,
traffic: AclTraffic.tcpPort(80),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW,
})
publicNetworkACL.addEntry('InboundHTTPSPublic', {
cidr: AclCidr.anyIpv4(),
ruleNumber: 110,
traffic: AclTraffic.tcpPort(443),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW,
})
// Individual IP address inbound access (20 reserved slots)
// http://checkip.amazonaws.com/
if (props.authorizedIPs?.length) {
props.authorizedIPs.forEach((ip, index) => {
publicNetworkACL.addEntry(`IPAllowPublic${index}`, {
cidr: AclCidr.ipv4(`${ip}/32`),
ruleNumber: 111 + index,
traffic: AclTraffic.tcpPort(22),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW,
})
})
}
publicNetworkACL.addEntry('EphemeralInboundPublic', {
cidr: AclCidr.anyIpv4(),
ruleNumber: 140,
traffic: AclTraffic.tcpPortRange(1024, 65535),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW,
})
// --------------------------------
// Outbound Public Entries
// --------------------------------
publicNetworkACL.addEntry('AllowAllOutbound', {
cidr: AclCidr.anyIpv4(),
ruleNumber: 100,
traffic: AclTraffic.allTraffic(),
direction: TrafficDirection.EGRESS,
ruleAction: Action.ALLOW,
})
if (props.vpc.privateSubnets.length) {
const privateNetworkACL = new NetworkAcl(this, 'PrivateNetworkACL', {
vpc: props.vpc,
subnetSelection: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
})
// --------------------------------
// Inbound Private Entries
// --------------------------------
// Allow all inbound traffic from public subnets
props.vpc
.selectSubnets({ subnetType: SubnetType.PUBLIC })
.subnets.forEach((sn, index) => {
privateNetworkACL.addEntry(`InboundPrivate${index}`, {
cidr: AclCidr.ipv4(sn.ipv4CidrBlock),
ruleNumber: 101 + index,
traffic: AclTraffic.allTraffic(),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW,
})
})
privateNetworkACL.addEntry('InboundEphemeralPrivate', {
cidr: AclCidr.anyIpv4(),
ruleNumber: 110,
traffic: AclTraffic.tcpPortRange(1024, 65535),
direction: TrafficDirection.INGRESS,
ruleAction: Action.ALLOW,
})
// --------------------------------
// Outbound Private Entries
// --------------------------------
privateNetworkACL.addEntry('AllowAllEgressPrivate', {
cidr: AclCidr.anyIpv4(),
ruleNumber: 100,
traffic: AclTraffic.allTraffic(),
direction: TrafficDirection.EGRESS,
ruleAction: Action.ALLOW,
})
}
}
}

View file

@ -1,73 +0,0 @@
import { IVpc, Peer, Port, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2'
import { CfnCacheCluster, CfnCacheClusterProps, CfnSubnetGroup } from 'aws-cdk-lib/aws-elasticache'
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'
import { Construct } from 'constructs'
export interface RedisProps {
vpc: IVpc
redisConfig: Pick<CfnCacheClusterProps, 'numCacheNodes' | 'cacheNodeType'>
}
export class Redis extends Construct {
public readonly redisUrl: string
constructor(scope: Construct, id: string, props: RedisProps) {
super(scope, id)
const redisSecurityGroup = new SecurityGroup(this, 'RedisSecurityGroup', {
vpc: props.vpc,
description: 'Redis instance security group',
allowAllOutbound: true,
})
const privateSubnets = props.vpc.selectSubnets({
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
})
const redisSubnetGroup = new CfnSubnetGroup(this, 'RedisSubnetGroup', {
description: 'Subnet group for Redis cluster',
subnetIds: privateSubnets.subnetIds,
cacheSubnetGroupName: 'redis-subnet',
})
const REDIS_PORT = 6379
const { logGroupName } = new LogGroup(this, 'RedisLogGroup', {
retention: RetentionDays.TWO_WEEKS,
})
const redisCluster = new CfnCacheCluster(this, 'RedisCluster', {
engine: 'redis',
cacheNodeType: props.redisConfig.cacheNodeType,
numCacheNodes: props.redisConfig.numCacheNodes,
autoMinorVersionUpgrade: true,
port: REDIS_PORT,
vpcSecurityGroupIds: [redisSecurityGroup.securityGroupId],
cacheSubnetGroupName: 'redis-subnet',
logDeliveryConfigurations: [
{
destinationDetails: { cloudWatchLogsDetails: { logGroup: logGroupName } },
destinationType: 'cloudwatch-logs',
logFormat: 'json',
logType: 'slow-log',
},
{
destinationDetails: {
cloudWatchLogsDetails: { logGroup: logGroupName },
},
destinationType: 'cloudwatch-logs',
logFormat: 'json',
logType: 'engine-log',
},
],
})
// Require the subnet group to be created before the cluster
redisCluster.node.addDependency(redisSubnetGroup)
// Open incoming traffic to the Redis port (e.g. 6379)
redisSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(REDIS_PORT))
this.redisUrl = `redis://${redisCluster.attrRedisEndpointAddress}:${redisCluster.attrRedisEndpointPort}`
}
}

View file

@ -1,295 +0,0 @@
import { Duration, Stack, StackProps, Tags } from 'aws-cdk-lib'
import { NetworkMode, Platform } from 'aws-cdk-lib/aws-ecr-assets'
import {
ContainerImage,
FargateService,
FargateTaskDefinition,
LogDriver,
PropagatedTagSource,
Secret as ECSSecret,
} from 'aws-cdk-lib/aws-ecs'
import {
ApplicationLoadBalancer,
ApplicationProtocol,
ApplicationProtocolVersion,
IApplicationLoadBalancer,
ListenerAction,
SslPolicy,
} from 'aws-cdk-lib/aws-elasticloadbalancingv2'
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
import { Construct } from 'constructs'
import { ServerStackContext } from '../utils/get-context'
import {
AllowedMethods,
CachePolicy,
Distribution,
OriginProtocolPolicy,
OriginRequestPolicy,
OriginSslPolicy,
PriceClass,
ViewerProtocolPolicy,
} from 'aws-cdk-lib/aws-cloudfront'
import { LoadBalancerV2Origin } from 'aws-cdk-lib/aws-cloudfront-origins'
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
import { SharedStack } from './shared-stack'
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'
interface ServerStackProps extends StackProps {
sharedStack: SharedStack
}
export class ServerStack extends Stack {
public readonly loadBalancer: IApplicationLoadBalancer
constructor(scope: Construct, id: string, props: ServerStackProps, ctx: ServerStackContext) {
super(scope, id, props)
const {
redisUrl,
userAccessKeyId,
userAccessSecretArn,
publicBucketName,
privateBucketName,
vpc,
cluster,
signerPubKeyId,
signerSecretId,
} = props.sharedStack
const taskDefinition = new FargateTaskDefinition(this, 'ServerTaskDefinition', {
memoryLimitMiB: +ctx.ComputeCapacity.memory,
cpu: ctx.ComputeCapacity.cpu,
})
// CDK does not add these permissions by default - https://github.com/aws/aws-cdk/issues/17156
// https://docs.aws.amazon.com/AmazonECS/latest/userguide/specifying-sensitive-data-secrets.html#secrets-iam
taskDefinition.addToExecutionRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue'],
resources: [userAccessSecretArn],
})
)
taskDefinition.addContainer('ServerContainer', {
logging: LogDriver.awsLogs({ streamPrefix: 'server-container' }),
image: ContainerImage.fromAsset('../../', {
file: 'apps/server/Dockerfile',
networkMode: NetworkMode.HOST,
target: 'prod',
platform: Platform.LINUX_AMD64, // explicitly define so can build on Macbook M1 (https://stackoverflow.com/a/71102144)
}),
environment: {
AWS_ACCESS_KEY_ID: userAccessKeyId,
NX_REDIS_URL: redisUrl,
NX_CDN_PUBLIC_BUCKET: publicBucketName,
NX_CDN_PRIVATE_BUCKET: privateBucketName,
NX_CDN_SIGNER_SECRET_ID: signerSecretId,
NX_CDN_SIGNER_PUBKEY_ID: signerPubKeyId,
...ctx.sharedEnv,
...ctx.Container.Env,
},
portMappings: [{ containerPort: +ctx.Container.Env.NX_PORT }],
entryPoint: ['sh', '-c'],
command: ['/bin/sh -c "node ./main.js"'],
healthCheck: {
command: [
'CMD-SHELL',
`curl --fail -s http://localhost:${ctx.Container.Env.NX_PORT}/health || exit 1`,
],
interval: Duration.seconds(10),
retries: 3,
startPeriod: Duration.seconds(10),
timeout: Duration.seconds(5),
},
stopTimeout: Duration.seconds(10), // waits 10 seconds after SIGTERM to fire SIGKILL
secrets: {
NX_DATABASE_URL: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(this, 'DatabaseUrlParam', {
parameterName: '/apps/maybe-app/NX_DATABASE_URL',
})
),
NX_DATABASE_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'DatabaseSecretParam',
{
parameterName: '/apps/maybe-app/NX_DATABASE_SECRET',
}
)
),
NX_SESSION_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'SessionSecretParam',
{
parameterName: '/apps/maybe-app/NX_SESSION_SECRET',
}
)
),
NX_AUTH0_CLIENT_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'Auth0ClientSecretParam',
{
parameterName: '/apps/maybe-app/NX_AUTH0_CLIENT_SECRET',
}
)
),
NX_AUTH0_MGMT_CLIENT_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'Auth0MgmtClientSecretParam',
{
parameterName: '/apps/maybe-app/NX_AUTH0_MGMT_CLIENT_SECRET',
}
)
),
NX_PLAID_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(this, 'PlaidSecretParam', {
parameterName: '/providers/NX_PLAID_SECRET',
})
),
NX_FINICITY_APP_KEY: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'FinicityAppKeyParam',
{
parameterName: '/providers/NX_FINICITY_APP_KEY',
}
)
),
NX_FINICITY_PARTNER_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'FinicityPartnerSecretParam',
{
parameterName: '/providers/NX_FINICITY_PARTNER_SECRET',
}
)
),
NX_POLYGON_API_KEY: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'PolygonApiKeyParam',
{
parameterName: '/providers/NX_POLYGON_API_KEY',
}
)
),
NX_STRIPE_SECRET_KEY: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'StripeSecretKeyParam',
{
parameterName: '/providers/NX_STRIPE_SECRET_KEY',
}
)
),
NX_STRIPE_WEBHOOK_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'StripeWebhookSecretParam',
{
parameterName: '/providers/NX_STRIPE_WEBHOOK_SECRET',
}
)
),
NX_POSTMARK_API_TOKEN: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'PostmarkApiTokenParam',
{
parameterName: '/providers/NX_POSTMARK_API_TOKEN',
}
)
),
// references the User access key created in SharedStack
AWS_SECRET_ACCESS_KEY: ECSSecret.fromSecretsManager(
Secret.fromSecretPartialArn(this, 'AwsSdkUserSecret', userAccessSecretArn)
),
},
})
const server = new FargateService(this, 'ServerService', {
cluster, // reference to SharedStack
taskDefinition,
enableECSManagedTags: true,
propagateTags: PropagatedTagSource.SERVICE,
desiredCount: ctx.DesiredTaskCount,
circuitBreaker: {
rollback: true,
},
})
this.loadBalancer = new ApplicationLoadBalancer(this, 'ServerLoadBalancer', {
vpc, // reference to SharedStack
internetFacing: true,
})
const httpsListener = this.loadBalancer.addListener('PublicListener', {
protocol: ApplicationProtocol.HTTPS,
port: 443,
open: true,
sslPolicy: SslPolicy.RECOMMENDED,
})
httpsListener.addCertificates('ServerLBCertificates', [
Certificate.fromCertificateArn(this, 'ServerCertificate', ctx.CertificateArn),
])
// Point the load balancer at the ECS task
httpsListener.addTargets('ECS', {
protocol: ApplicationProtocol.HTTP,
protocolVersion: ApplicationProtocolVersion.HTTP1,
targets: [server],
deregistrationDelay: Duration.seconds(10),
healthCheck: {
path: '/health',
healthyThresholdCount: 2,
timeout: Duration.seconds(3),
interval: Duration.seconds(5),
},
})
// Redirect all HTTP traffic to HTTPs
this.loadBalancer.addListener('RedirectToHTTPSListener', {
protocol: ApplicationProtocol.HTTP,
port: 80,
open: true,
defaultAction: ListenerAction.redirect({
port: '443',
protocol: ApplicationProtocol.HTTPS,
permanent: true,
}),
})
// WAF => Cloudfront => ALB
new Distribution(this, 'ServerCloudfrontDistribution', {
comment: 'ALB for ECS Fargate server',
defaultBehavior: {
origin: new LoadBalancerV2Origin(this.loadBalancer, {
protocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
originSslProtocols: [OriginSslPolicy.TLS_V1_1],
}),
allowedMethods: AllowedMethods.ALLOW_ALL,
cachePolicy: CachePolicy.CACHING_DISABLED,
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
priceClass: PriceClass.PRICE_CLASS_100,
webAclId: ctx.WAFArn,
certificate: Certificate.fromCertificateArn(
this,
'CloudfrontCertificate',
ctx.Cloudfront.CertificateArn
),
domainNames: ctx.Cloudfront.CNAMES,
})
// Put a tag on this service so we can track it easier in Cost Explorer
Tags.of(server).add('ecs-service-name', 'server')
}
}

View file

@ -1,229 +0,0 @@
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'
import { Distribution, KeyGroup, PriceClass, PublicKey } from 'aws-cdk-lib/aws-cloudfront'
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'
import {
CfnRoute,
GatewayVpcEndpointAwsService,
InterfaceVpcEndpointAwsService,
IVpc,
Vpc,
} from 'aws-cdk-lib/aws-ec2'
import { Cluster, ICluster } from 'aws-cdk-lib/aws-ecs'
import { AccessKey, Policy, PolicyStatement, User } from 'aws-cdk-lib/aws-iam'
import { Bucket, CfnBucket, HttpMethods } from 'aws-cdk-lib/aws-s3'
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
import { Construct } from 'constructs'
import { JumpBox } from '../constructs/jump-box'
import { NetworkACLConfig } from '../constructs/network-acl'
import { Redis } from '../constructs/redis'
import { SharedStackContext } from '../utils/get-context'
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
interface SharedStackProps extends StackProps {}
/**
* Shared resources that workloads are deployed to
*
* @see - https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html
*/
export class SharedStack extends Stack {
public readonly vpc: IVpc
public readonly cluster: ICluster
public readonly redisUrl: string
public readonly userAccessSecretArn: string
public readonly userAccessKeyId: string
public readonly privateBucketName: string
public readonly publicBucketName: string
public readonly signerSecretId: string
public readonly signerPubKeyId: string
constructor(scope: Construct, id: string, props: SharedStackProps, ctx: SharedStackContext) {
super(scope, id, props)
this.vpc = new Vpc(this, 'MaybeAppVPC', {
maxAzs: 2,
natGateways: ctx.VPC.NATCount,
})
new NetworkACLConfig(this, 'MaybeAppVPCNetworkACLConfig', {
vpc: this.vpc,
authorizedIPs: ctx.VPC.IPAllowList,
})
// Fargate >= 1.4.0 requires S3 Gateway, ECR, ECR Docker, and Cloudwatch Logs
// https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-vpc-endpoint-considerations
this.vpc.addGatewayEndpoint('S3GatewayEndpoint', {
service: GatewayVpcEndpointAwsService.S3,
})
this.vpc.addInterfaceEndpoint('ECRInterfaceEndpoint', {
service: InterfaceVpcEndpointAwsService.ECR,
})
this.vpc.addInterfaceEndpoint('DockerInterfaceEndpoint', {
service: InterfaceVpcEndpointAwsService.ECR_DOCKER,
})
// Containers write logs directly to Cloudwatch
this.vpc.addInterfaceEndpoint('CloudwatchLogsInterfaceEndpoint', {
service: InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
})
// Open a communication channel with TimescaleDB
if (ctx.PeeringCnxId) {
const addPeeringRoute = (routeTableId: string, index: number, subnetType: string) => {
new CfnRoute(this, `PeeringRoute-${subnetType}-${index}`, {
destinationCidrBlock: ctx.PeeringCnxCidr,
routeTableId,
vpcPeeringConnectionId: ctx.PeeringCnxId,
})
}
// Allows our private services to connect to DB
this.vpc.privateSubnets.forEach((subnet, index) =>
addPeeringRoute(subnet.routeTable.routeTableId, index, 'private')
)
// This allows our JumpBox SSH tunnel to work
this.vpc.publicSubnets.forEach((subnet, index) =>
addPeeringRoute(subnet.routeTable.routeTableId, index, 'public')
)
}
// The ECS Cluster that all ECS services and tasks are deployed to
this.cluster = new Cluster(this, 'ECSCluster', { vpc: this.vpc })
const redis = new Redis(this, 'Redis', {
vpc: this.vpc,
redisConfig: {
numCacheNodes: ctx.Redis.count,
cacheNodeType: ctx.Redis.size,
},
})
this.redisUrl = redis.redisUrl
// For connecting to DB through EC2 instance as SSH tunnel
new JumpBox(this, 'JumpBox', { vpc: this.vpc }, ctx)
// Where we put public assets like fonts, logos, etc.
// Everything stored here is publicly available over our CDN
const publicBucket = new Bucket(this, 'Public', {
removalPolicy: RemovalPolicy.RETAIN,
})
// WORM compliant bucket to store CDN assets such as AMA uploads
const privateBucket = new Bucket(this, 'Assets', {
versioned: true,
removalPolicy: RemovalPolicy.RETAIN,
cors: [
{
allowedHeaders: [
'Authorization',
'x-amz-date',
'x-amz-content-sha256',
'content-type',
],
allowedMethods: [HttpMethods.GET, HttpMethods.POST],
allowedOrigins: ctx.UploadOrigins,
},
],
})
const privateBucketRef = privateBucket.node.defaultChild as CfnBucket
privateBucketRef.addPropertyOverride('ObjectLockEnabled', true)
privateBucketRef.addPropertyOverride('ObjectLockConfiguration.ObjectLockEnabled', 'Enabled')
privateBucketRef.addPropertyOverride(
'ObjectLockConfiguration.Rule.DefaultRetention.Years',
5
)
privateBucketRef.addPropertyOverride(
'ObjectLockConfiguration.Rule.DefaultRetention.Mode',
'GOVERNANCE'
)
const signerPublicKey = StringParameter.fromStringParameterName(
this,
'SignerPKParam',
'/apps/maybe-app/CLOUDFRONT_SIGNER1_PUB'
)
const signerPubKey = new PublicKey(this, 'SignerPub', {
encodedKey: signerPublicKey.stringValue,
})
// server app will grab this ID as a reference
this.signerPubKeyId = signerPubKey.publicKeyId
const signerPrivateKey = Secret.fromSecretNameV2(
this,
'SignerPriv',
'/apps/maybe-app/CLOUDFRONT_SIGNER1_PRIV'
)
// server app will dynamically grab this (via SDK) by its ID to sign urls
this.signerSecretId = signerPrivateKey.secretName
// This group holds public keys that can sign URLs and cookies to serve restricted content from the Distribution
const keyGroup = new KeyGroup(this, 'KeyGroup', {
items: [signerPubKey],
})
const distribution = new Distribution(this, 'Distribution', {
comment: 'Maybe App Cloudfront CDN',
domainNames: ctx.Cloudfront.CNAMES,
certificate: Certificate.fromCertificateArn(
this,
'CdnCert',
ctx.Cloudfront.CertificateArn
),
defaultBehavior: {
origin: new S3Origin(publicBucket),
},
additionalBehaviors: {
// All content served here requires signed urls to view
'/private/*': {
origin: new S3Origin(privateBucket),
trustedKeyGroups: [keyGroup],
},
},
priceClass: PriceClass.PRICE_CLASS_100,
})
/**
* User that AWS SDK will use within server and workers apps
*
* Permissions added as-needed here.
*/
const user = new User(this, 'SdkUser')
user.attachInlinePolicy(
new Policy(this, 'SdkUserPolicy', {
statements: [
new PolicyStatement({
actions: ['s3:PutObject', 's3:GetObject', 's3:GetObjectAttributes'],
resources: [`${privateBucket.bucketArn}/*`, `${publicBucket.bucketArn}/*`],
}),
new PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
// Can access any key stored under the /apps/maybe-app path
resources: [
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:/apps/maybe-app/*`,
],
}),
],
})
)
// Access key secret safely stored in secrets manager (and retrieved in server/workers stacks)
const accessKey = new AccessKey(this, 'SdkUserAcessKey', { user })
const accessKeySecret = new Secret(this, 'SdkUserAccessKeySecret', {
secretStringValue: accessKey.secretAccessKey,
})
this.userAccessKeyId = accessKey.accessKeyId
this.userAccessSecretArn = accessKeySecret.secretArn
this.privateBucketName = privateBucket.bucketName
this.publicBucketName = publicBucket.bucketName
}
}

View file

@ -1,30 +0,0 @@
import { Stack, StackProps } from 'aws-cdk-lib'
import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'
import { Construct } from 'constructs'
import { GithubActionsRunner } from '../constructs/github-actions-runner/github-actions-runner'
import { NetworkACLConfig } from '../constructs/network-acl'
import { ToolsStackContext } from '../utils/get-context'
interface ToolsStackProps extends StackProps {}
/**
* Resources deployed to the Deployments account for CI/CD purposes
*/
export class ToolsStack extends Stack {
constructor(scope: Construct, id: string, props: ToolsStackProps, ctx: ToolsStackContext) {
super(scope, id, props)
const vpc = new Vpc(this, 'ToolsVPC', {
maxAzs: 1,
natGateways: 0,
subnetConfiguration: [{ subnetType: SubnetType.PUBLIC, name: 'public' }],
})
new NetworkACLConfig(this, 'ToolsNetworkACL', {
vpc,
authorizedIPs: ctx.VPC.IPAllowList,
})
new GithubActionsRunner(this, 'GithubActionsRunnerInstance', { vpc }, ctx)
}
}

View file

@ -1,179 +0,0 @@
import { Duration, Stack, StackProps, Tags } from 'aws-cdk-lib'
import { NetworkMode, Platform } from 'aws-cdk-lib/aws-ecr-assets'
import {
ContainerImage,
FargateService,
FargateTaskDefinition,
LogDrivers,
PropagatedTagSource,
Secret as ECSSecret,
} from 'aws-cdk-lib/aws-ecs'
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
import { Construct } from 'constructs'
import { WorkersStackContext } from '../utils/get-context'
import { SharedStack } from './shared-stack'
interface WorkersStackProps extends StackProps {
sharedStack: SharedStack
}
export class WorkersStack extends Stack {
constructor(scope: Construct, id: string, props: WorkersStackProps, ctx: WorkersStackContext) {
super(scope, id, props)
const {
redisUrl,
userAccessKeyId,
userAccessSecretArn,
publicBucketName,
privateBucketName,
cluster,
} = props.sharedStack
const workerTask = new FargateTaskDefinition(this, 'WorkerTaskDefinition', {
memoryLimitMiB: +ctx.ComputeCapacity.memory,
cpu: ctx.ComputeCapacity.cpu,
})
// CDK does not add these permissions by default - https://github.com/aws/aws-cdk/issues/17156
// https://docs.aws.amazon.com/AmazonECS/latest/userguide/specifying-sensitive-data-secrets.html#secrets-iam
workerTask.addToExecutionRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue'],
resources: [userAccessSecretArn],
})
)
workerTask.addContainer('worker-container', {
logging: LogDrivers.awsLogs({ streamPrefix: 'WorkerLog' }),
image: ContainerImage.fromAsset('../../', {
file: 'apps/workers/Dockerfile',
networkMode: NetworkMode.HOST,
target: 'prod',
platform: Platform.LINUX_AMD64, // explicitly define so can build on Macbook M1 (https://stackoverflow.com/a/71102144)
}),
environment: {
AWS_ACCESS_KEY_ID: userAccessKeyId,
NX_REDIS_URL: redisUrl,
NX_CDN_PUBLIC_BUCKET: publicBucketName,
NX_CDN_PRIVATE_BUCKET: privateBucketName,
...ctx.sharedEnv,
...ctx.Container.Env,
},
portMappings: [{ containerPort: +ctx.Container.Env.NX_PORT }],
entryPoint: ['sh', '-c'],
command: ['/bin/sh -c "node ./main.js"'],
healthCheck: {
command: [
'CMD-SHELL',
`curl --fail -s http://localhost:${ctx.Container.Env.NX_PORT}/health || exit 1`,
],
interval: Duration.seconds(10),
retries: 3,
startPeriod: Duration.seconds(10),
timeout: Duration.seconds(5),
},
stopTimeout: Duration.seconds(10),
secrets: {
NX_DATABASE_URL: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(this, 'DatabaseUrlParam', {
parameterName: '/apps/maybe-app/NX_DATABASE_URL',
})
),
NX_DATABASE_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'DatabaseSecretParam',
{
parameterName: '/apps/maybe-app/NX_DATABASE_SECRET',
}
)
),
NX_AUTH0_MGMT_CLIENT_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'Auth0MgmtClientSecretParam',
{
parameterName: '/apps/maybe-app/NX_AUTH0_MGMT_CLIENT_SECRET',
}
)
),
NX_PLAID_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(this, 'PlaidSecretParam', {
parameterName: '/providers/NX_PLAID_SECRET',
})
),
NX_FINICITY_APP_KEY: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'FinicityAppKeyParam',
{
parameterName: '/providers/NX_FINICITY_APP_KEY',
}
)
),
NX_FINICITY_PARTNER_SECRET: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'FinicityPartnerSecretParam',
{
parameterName: '/providers/NX_FINICITY_PARTNER_SECRET',
}
)
),
NX_POLYGON_API_KEY: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'PolygonApiKeyParam',
{
parameterName: '/providers/NX_POLYGON_API_KEY',
}
)
),
NX_POSTMARK_API_TOKEN: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'PostmarkApiTokenParam',
{
parameterName: '/providers/NX_POSTMARK_API_TOKEN',
}
)
),
NX_STRIPE_SECRET_KEY: ECSSecret.fromSsmParameter(
StringParameter.fromSecureStringParameterAttributes(
this,
'StripeSecretKeyParam',
{
parameterName: '/providers/NX_STRIPE_SECRET_KEY',
}
)
),
// references the User access key created in SharedStack
AWS_SECRET_ACCESS_KEY: ECSSecret.fromSecretsManager(
Secret.fromSecretPartialArn(this, 'AwsSdkUserSecret', userAccessSecretArn)
),
},
})
const workerService = new FargateService(this, 'WorkerService', {
cluster, // reference from SharedStack
taskDefinition: workerTask,
enableECSManagedTags: true,
propagateTags: PropagatedTagSource.SERVICE,
// ECS deployment config
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html#deployment-circuit-breaker
desiredCount: ctx.DesiredTaskCount,
circuitBreaker: {
rollback: true,
},
})
// Tag service for easier tracking in cost explorer
Tags.of(workerService).add('ecs-service-name', 'workers')
}
}

View file

@ -1,231 +0,0 @@
import { App } from 'aws-cdk-lib'
import { z } from 'zod'
const toStr = (v: number) => v.toString()
/**
* Fargate compute capacity schema
*
* @see https://docs.aws.amazon.com/AmazonECS/latest/userguide/fargate-task-defs.html#fargate-tasks-size
*/
const fargateComputeSchema = z.union([
z
.object({
cpu: z.literal(512),
memory: z.enum(['1024', '2048', '3072', '4096']),
})
.strict(),
z
.object({
cpu: z.literal(1024),
memory: z.enum(['2048', '3072', '4096', '5120', '6144', '7168', '8192']),
})
.strict(),
z
.object({
cpu: z.literal(2048),
memory: z.enum(['4096', '5120', '6144', '7168', '8192', '9216', '10240']),
})
.strict(),
z
.object({
cpu: z.literal(4096),
memory: z.enum(['4096', '5120', '6144', '7168', '8192', '9216', '10240']),
})
.strict(),
])
const maybeAppSharedEnv = z
.object({
NX_API_URL: z.string(),
NX_CDN_URL: z.string(),
NODE_ENV: z.enum(['production', 'development']),
NX_AUTH0_AUDIENCE: z.literal('https://maybe-finance-api/v1'),
NX_PLAID_CLIENT_ID: z.literal('REPLACE_THIS'),
NX_PLAID_ENV: z.string(),
NX_FINICITY_PARTNER_ID: z.literal('REPLACE_THIS'),
NX_FINICITY_ENV: z.enum(['sandbox', 'production']),
NX_CLIENT_URL: z.string(),
NX_CLIENT_URL_CUSTOM: z.string(),
NX_AUTH0_DOMAIN: z.string(),
NX_AUTH0_CUSTOM_DOMAIN: z.string(),
NX_AUTH0_CLIENT_ID: z.string(),
NX_AUTH0_MGMT_CLIENT_ID: z.string(),
NX_SENTRY_ENV: z.string(),
NX_POSTMARK_FROM_ADDRESS: z.string(),
NX_STRIPE_PREMIUM_YEARLY_PRICE_ID: z.string().optional(),
NX_STRIPE_PREMIUM_MONTHLY_PRICE_ID: z.string().optional(),
})
.strict()
const maybeAppSharedStackContext = z
.object({
PeeringCnxId: z.string().optional(),
PeeringCnxCidr: z.string(),
VPC: z
.object({
NATCount: z.number().lte(2),
IPAllowList: z.string().array().max(20), // Limit to 20 addresses to avoid rule priority conflicts
})
.strict(),
Redis: z
.object({
size: z.enum([
'cache.t4g.micro',
'cache.t4g.small',
'cache.t4g.medium',
'cache.m5.large',
'cache.m6g.large',
'cache.m6g.xlarge',
'cache.m6g.2xlarge',
]),
count: z.number().lte(5),
})
.strict(),
Cloudfront: z
.object({
CertificateArn: z.string(), // located in us-east-1 region
CNAMES: z.string().array(),
})
.strict(),
UploadOrigins: z.array(z.string()).min(1),
})
.strict()
const maybeAppServerStackContext = z.object({
CertificateArn: z.string(),
DesiredTaskCount: z.number(),
WAFArn: z.string(),
ComputeCapacity: fargateComputeSchema,
Cloudfront: z
.object({
CNAMES: z.string().array(),
CertificateArn: z.string(), // located in us-east-1 region
})
.strict(),
Container: z
.object({
Env: z
.object({
NX_CORS_ORIGINS: z.string(),
NX_MORGAN_LOG_LEVEL: z.string(),
NX_PORT: z.number().transform(toStr),
NX_SENTRY_DSN: z.string(),
})
.strict(),
})
.strict(),
})
const maybeAppWorkersStackContext = z
.object({
DesiredTaskCount: z.number(),
ComputeCapacity: fargateComputeSchema,
Container: z
.object({
Env: z
.object({
NX_PORT: z.number().transform(toStr),
NX_SENTRY_DSN: z.string(),
})
.strict(),
})
.strict(),
})
.strict()
const maybeAppStackContexts = z
.object({
Shared: maybeAppSharedStackContext,
Server: maybeAppServerStackContext,
Workers: maybeAppWorkersStackContext,
})
.strict()
const stagingEnvSchema = z
.object({
ENV_NAME: z.literal('staging'),
AWS_ACCOUNT: z.literal('REPLACE_THIS'),
DEFAULT_AWS_REGION: z.literal('us-west-2'),
sharedEnv: maybeAppSharedEnv,
stackContexts: maybeAppStackContexts,
})
.strict()
const prodEnvSchema = z
.object({
ENV_NAME: z.literal('production'),
AWS_ACCOUNT: z.literal('541001830411'),
DEFAULT_AWS_REGION: z.literal('us-west-2'),
sharedEnv: maybeAppSharedEnv,
stackContexts: maybeAppStackContexts,
})
.strict()
const toolsStackContexts = z
.object({
Tools: z.object({
VPC: z
.object({
IPAllowList: z.string().array().max(20),
})
.strict(),
GithubRunner: z
.object({
SSMKeyArn: z.string(),
GithubTokenArn: z.string(),
})
.strict(),
}),
})
.strict()
const toolsSharedEnv = z.object({}).strict()
const toolsEnvSchema = z
.object({
ENV_NAME: z.literal('tools'),
AWS_ACCOUNT: z.literal('REPLACE_THIS'),
DEFAULT_AWS_REGION: z.literal('us-west-2'),
sharedEnv: toolsSharedEnv,
stackContexts: toolsStackContexts,
})
.strict()
// Determines what account the configuration is deployed to
const envSchema = z
.object({
tools: toolsEnvSchema,
staging: stagingEnvSchema,
production: prodEnvSchema,
})
.strict()
export const getContext = (app: App) => {
const envKeySchema = z.enum(['tools', 'staging', 'production'])
const envKey = envKeySchema.parse(process.env.CDK_ENV)
const envRaw = app.node.tryGetContext('environments')
if (!envRaw) throw new Error(`cdk.json is in an invalid config state`)
const env = envSchema.parse(envRaw)
console.log(`Environment: ${envKey}`)
console.log(JSON.stringify(env[envKey], null, 2))
return env[envKey]
}
type MaybeAppContext<TContext> = { sharedEnv: z.infer<typeof maybeAppSharedEnv> } & {
DEFAULT_AWS_REGION: string
} & TContext
export type SharedStackContext = MaybeAppContext<z.infer<typeof maybeAppStackContexts>['Shared']>
export type ServerStackContext = MaybeAppContext<z.infer<typeof maybeAppStackContexts>['Server']>
export type WorkersStackContext = MaybeAppContext<z.infer<typeof maybeAppStackContexts>['Workers']>
type ToolsContext<TContext> = { sharedEnv: z.infer<typeof toolsSharedEnv> } & {
DEFAULT_AWS_REGION: string
} & TContext
export type ToolsStackContext = ToolsContext<z.infer<typeof toolsStackContexts>['Tools']>

View file

@ -1,30 +0,0 @@
{
"name": "maybe-app",
"version": "0.1.0",
"bin": {
"maybe-app": "bin/maybe-app.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "CDK_ENV=staging cdk",
"cdk:ls": "yarn cdk ls --profile=maybe_tools",
"cdk:deploy:shared": "yarn cdk deploy SharedStack --profile=maybe_tools"
},
"devDependencies": {
"@types/jest": "^26.0.10",
"@types/node": "10.17.27",
"aws-cdk": "^2.52.0",
"jest": "^26.4.2",
"ts-jest": "^26.2.0",
"ts-node": "^10.9.1",
"typescript": "^4.6.4"
},
"dependencies": {
"aws-cdk-lib": "^2.52.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.16",
"zod": "^3.14.4"
}
}

View file

@ -1,202 +0,0 @@
import * as cdk from 'aws-cdk-lib'
import { Template } from 'aws-cdk-lib/assertions'
import { SharedStack } from '../lib/stacks/shared-stack'
import { ServerStack } from '../lib/stacks/server-stack'
import { WorkersStack } from '../lib/stacks/workers-stack'
import { getContext } from '../lib/utils/get-context'
// Workaround - by default, context is not automatically passed to tests
// https://github.com/aws/aws-cdk/issues/5149#issuecomment-1084788745
import cdkJsonRaw from '../cdk.json'
// ======================================================================
// ===================== STAGING TESTS ==================================
// ======================================================================
test('Shared staging stack created', () => {
process.env['CDK_ENV'] = 'staging'
const app = new cdk.App({ context: cdkJsonRaw.context })
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
const sharedStack = new SharedStack(
app,
'SharedStagingStack',
{},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
)
const template = Template.fromStack(sharedStack)
// VPC created correctly
template.resourceCountIs('AWS::EC2::VPC', 1)
template.resourceCountIs('AWS::EC2::Subnet', 4)
template.resourceCountIs('AWS::EC2::NatGateway', 1)
template.resourceCountIs('AWS::EC2::RouteTable', 4)
template.resourceCountIs('AWS::EC2::EIP', 1)
template.resourceCountIs('AWS::EC2::InternetGateway', 1)
// Redis cluster created
template.resourceCountIs('AWS::ElastiCache::CacheCluster', 1)
// ECS cluster created
template.resourceCountIs('AWS::ECS::Cluster', 1)
})
test('Server staging stack created', () => {
process.env['CDK_ENV'] = 'staging'
const app = new cdk.App({ context: cdkJsonRaw.context })
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
const sharedStack = new SharedStack(
app,
'SharedStagingStack',
{},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
)
const serverStack = new ServerStack(
app,
'ServerStagingStack',
{
sharedStack,
},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Server }
)
const template = Template.fromStack(serverStack)
template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1)
template.resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 2)
template.resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1)
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
template.resourceCountIs('AWS::ECS::Service', 1)
})
test('Workers staging stack created', () => {
process.env['CDK_ENV'] = 'staging'
const app = new cdk.App({ context: cdkJsonRaw.context })
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
const sharedStack = new SharedStack(
app,
'SharedStagingStack',
{},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
)
const workersStack = new WorkersStack(
app,
'WorkersStagingStack',
{
sharedStack,
},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Workers }
)
const template = Template.fromStack(workersStack)
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
template.resourceCountIs('AWS::ECS::Service', 1)
})
// ======================================================================
// ================== PRODUCTION TESTS ==================================
// ======================================================================
test('Shared production stack created', () => {
process.env['CDK_ENV'] = 'production'
const app = new cdk.App({ context: cdkJsonRaw.context })
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
const sharedStack = new SharedStack(
app,
'SharedStack',
{},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
)
const template = Template.fromStack(sharedStack)
// VPC created correctly
template.resourceCountIs('AWS::EC2::VPC', 1)
template.resourceCountIs('AWS::EC2::Subnet', 4)
template.resourceCountIs('AWS::EC2::NatGateway', 2)
template.resourceCountIs('AWS::EC2::RouteTable', 4)
template.resourceCountIs('AWS::EC2::EIP', 2)
template.resourceCountIs('AWS::EC2::InternetGateway', 1)
// Redis cluster created
template.resourceCountIs('AWS::ElastiCache::CacheCluster', 1)
// ECS cluster created
template.resourceCountIs('AWS::ECS::Cluster', 1)
})
test('Server production stack created', () => {
process.env['CDK_ENV'] = 'production'
const app = new cdk.App({ context: cdkJsonRaw.context })
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
const sharedStack = new SharedStack(
app,
'SharedProductionStack',
{},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
)
const serverStack = new ServerStack(
app,
'ServerProductionStack',
{
sharedStack,
},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Server }
)
const template = Template.fromStack(serverStack)
template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1)
template.resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 2)
template.resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1)
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
template.resourceCountIs('AWS::ECS::Service', 1)
})
test('Workers production stack created', () => {
process.env['CDK_ENV'] = 'production'
const app = new cdk.App({ context: cdkJsonRaw.context })
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
const sharedStack = new SharedStack(
app,
'SharedProductionStack',
{},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
)
const workersStack = new WorkersStack(
app,
'WorkersProductionStack',
{
sharedStack,
},
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Workers }
)
const template = Template.fromStack(workersStack)
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
template.resourceCountIs('AWS::ECS::Service', 1)
})

View file

@ -1,25 +0,0 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"lib": ["es2018"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"typeRoots": ["./node_modules/@types"],
"resolveJsonModule": true,
"esModuleInterop": true
},
"exclude": ["node_modules", "cdk.out"]
}

File diff suppressed because it is too large Load diff

View file

@ -147,7 +147,7 @@ export default function InstitutionGrid({
<Image
className="cursor-pointer hover:opacity-90"
key={img.alt}
loader={BrowserUtil.s3Loader}
loader={BrowserUtil.enhancerizerLoader}
src={`financial-institutions/${img.src}`}
alt={img.alt}
layout="responsive"

View file

@ -6,6 +6,7 @@ import { Button } from '@maybe-finance/design-system'
import { BrowserUtil, useAccountContext, useUserAccountContext } from '@maybe-finance/client/shared'
import { ExampleApp } from '../../ExampleApp'
import { AiOutlineLoading3Quarters as LoadingIcon } from 'react-icons/ai'
import Image from 'next/legacy/image'
import type { StepProps } from '../StepProps'
export function AddFirstAccount({ title, onNext }: StepProps) {
@ -54,10 +55,9 @@ export function AddFirstAccount({ title, onNext }: StepProps) {
['Citibank', 'citi'],
].map(([name, src]) => (
<div key={name} className="h-6">
<img
src={BrowserUtil.s3Loader({
src: `financial-institutions/white/${src}.svg`,
})}
<Image
loader={BrowserUtil.enhancerizerLoader}
src={`financial-institutions/white/${src}.svg`}
alt={name}
/>
</div>

View file

@ -11,7 +11,3 @@ export function enhancerizerLoader({ src, width }: ImageLoaderProps): string {
return `https://enhancerizer.maybe.co/images?${queryString}`
}
export function s3Loader({ src }: Pick<ImageLoaderProps, 'src'>): string {
return `https://assets.maybe.co/images/${src}`
}

View file

@ -4,4 +4,3 @@ export * from './queue'
export * from './cache.service'
export * from './market-data.service'
export * from './pg.service'
export * from './s3.service'

View file

@ -1,62 +0,0 @@
import {
type S3Client,
type PutObjectCommandOutput,
type PutObjectCommandInput,
type DeleteObjectCommandInput,
type DeleteObjectCommandOutput,
PutObjectCommand,
DeleteObjectCommand,
} from '@aws-sdk/client-s3'
import { v4 as uuid } from 'uuid'
type UploadOptions = {
bucketKey: BucketKey
Key?: string
} & Omit<PutObjectCommandInput, 'Bucket' | 'Key'>
type DeleteOptions = {
bucketKey: BucketKey
Key: string
} & Omit<DeleteObjectCommandInput, 'Bucket' | 'Key'>
export interface IS3Service {
upload(opts?: UploadOptions): Promise<PutObjectCommandOutput & { Key: string }>
delete(opts: DeleteOptions): Promise<DeleteObjectCommandOutput & { Key: string }>
}
export type BucketKey = 'public' | 'private'
export class S3Service implements IS3Service {
constructor(readonly cli: S3Client, readonly buckets: Record<BucketKey, string>) {}
async upload({ bucketKey, Key = uuid(), ...rest }: UploadOptions) {
const uploadRes = await this.cli.send(
new PutObjectCommand({
Bucket: this.buckets[bucketKey],
Key,
...rest,
})
)
return {
...uploadRes,
Key,
}
}
async delete({ bucketKey, Key, ...rest }: DeleteOptions) {
const deleteRes = await this.cli.send(
new DeleteObjectCommand({
Bucket: this.buckets[bucketKey],
Key,
...rest,
})
)
return {
...deleteRes,
Key,
}
}
}

View file

@ -40,10 +40,6 @@
"dependencies": {
"@auth0/auth0-react": "^2.0.0",
"@auth0/nextjs-auth0": "^2.0.0",
"@aws-sdk/client-s3": "^3.231.0",
"@aws-sdk/client-secrets-manager": "^3.235.0",
"@aws-sdk/cloudfront-signer": "^3.229.0",
"@aws-sdk/s3-presigned-post": "^3.234.0",
"@bull-board/express": "^4.6.4",
"@casl/ability": "^6.3.2",
"@casl/prisma": "^1.4.1",
@ -71,7 +67,6 @@
"@trpc/react-query": "^10.4.3",
"@trpc/server": "^10.4.3",
"@types/sanitize-html": "^2.8.0",
"@uppy/aws-s3": "^3.0.4",
"@uppy/core": "^3.0.4",
"@uppy/dashboard": "^3.2.0",
"@uppy/drag-drop": "^3.0.1",

View file

@ -1,12 +0,0 @@
#!/bin/bash
if test -f ~/.ssh/jumpbox_key_production; then
echo "Key exists, starting session..."
else
echo -e "Prereqs: \n\n1. Create a file called ~/.ssh/jumpbox_key_production\n2. Get key from 1Password, paste into file\n3. Run chmod 400 ~/.ssh/jumpbox_key_production\n\n"
fi
echo "Enter Postgres string in host:port format"
read PG_HOST_PORT
ssh -i ~/.ssh/jumpbox_key_production -L 5555:$PG_HOST_PORT ubuntu@ec2-34-222-246-3.us-west-2.compute.amazonaws.com

View file

@ -1,12 +0,0 @@
#!/bin/bash
if test -f ~/.ssh/jumpbox_key_staging; then
echo "Key exists, starting session..."
else
echo -e "Prereqs: \n\n1. Create a file called ~/.ssh/jumpbox_key_staging\n2. Get key from 1Password, paste into file\n3. Run chmod 400 ~/.ssh/jumpbox_key_staging\n\n"
fi
echo "Enter Postgres string in host:port format"
read PG_HOST_PORT
ssh -i ~/.ssh/jumpbox_key_staging -L 5555:$PG_HOST_PORT ubuntu@ec2-54-185-10-3.us-west-2.compute.amazonaws.com

View file

@ -15,10 +15,6 @@ console.log(`CI_PROJECT_NAME: ${CI_PROJECT_NAME}`)
* from a deploy hook, and therefore, all manual builds would be cancelled without this logic here.
* @see https://github.com/vercel/community/discussions/285#discussioncomment-1696833
*
* We skip automatic deploys to main because our AWS resources (server, workers)
* take longer to deploy. Instead, we programmatically deploy after these AWS
* services have successfully deployed to minimize any mismatches in app versions.
*
* We deploy the staging-client project on merge to main to avoid conflicting deploys
* with our main app's PR preview deploys.
*/

1318
yarn.lock

File diff suppressed because it is too large Load diff