diff --git a/cloud/bin/cloud.ts b/cloud/bin/cloud.ts index 68acd0a62..3903f9dd9 100644 --- a/cloud/bin/cloud.ts +++ b/cloud/bin/cloud.ts @@ -8,6 +8,7 @@ import { resourceDescription, stackName, ApiStack, + UiStack, } from '../lib'; const app = new App(); @@ -22,11 +23,17 @@ const tags = { const generateStackName = stackName(app); const generateDescription = resourceDescription(app); -// Don't need this stack, yet... Or ever? Will ask Pete C. +const uiStack = new UiStack(app, generateStackName('ui'), { + tags, + description: generateDescription('UI stack'), +}); + +// Don't need this stack yet. /* const authStack = new AuthStack(app, generateStackName('auth'), { tags, description: generateDescription('Auth stack'), + webappUrl: uiStack.cloudfrontUrl, }); */ @@ -36,4 +43,5 @@ new ApiStack(app, generateStackName('api'), { // userPool: authStack.userPool, // userPoolClient: authStack.userPoolClient, // userPoolDomain: authStack.userPoolDomain, + webappUrl: uiStack.cloudfrontUrl, }); diff --git a/cloud/lib/api-stack.ts b/cloud/lib/api-stack.ts index f3ea5e3c8..ecae28be2 100644 --- a/cloud/lib/api-stack.ts +++ b/cloud/lib/api-stack.ts @@ -20,13 +20,14 @@ type ApiStackProps = StackProps & { // userPool: UserPool; // userPoolClient: UserPoolClient; // userPoolDomain: UserPoolDomain; + webappUrl: string; }; export class ApiStack extends Stack { constructor(scope: Construct, id: string, props: ApiStackProps) { super(scope, id, props); // TODO Enable cognito auth - //const { userPool, userPoolClient, userPoolDomain } = props; + const { /*userPool, userPoolClient, userPoolDomain,*/ webappUrl } = props; const generateResourceName = resourceName(scope); @@ -68,6 +69,7 @@ export class ApiStack extends Stack { environment: { NODE_ENV: 'production', PORT: `${containerPort}`, + CORS_ALLOW_ORIGIN: webappUrl, }, secrets: { OPENAI_API_KEY: EnvSecret.fromSecretsManager( diff --git a/cloud/lib/auth-stack.ts b/cloud/lib/auth-stack.ts index be965edfd..8f3bcef9a 100644 --- a/cloud/lib/auth-stack.ts +++ b/cloud/lib/auth-stack.ts @@ -14,12 +14,16 @@ import { Construct } from 'constructs'; import { resourceName } from './resourceNamingUtils'; +type AuthStackProps = StackProps & { + webappUrl: string; +}; + export class AuthStack extends Stack { userPool: UserPool; userPoolClient: UserPoolClient; userPoolDomain: UserPoolDomain; - constructor(scope: Construct, id: string, props: StackProps) { + constructor(scope: Construct, id: string, props: AuthStackProps) { super(scope, id, props); const azureTenantId = process.env.AZURE_TENANT_ID; @@ -83,6 +87,7 @@ export class AuthStack extends Stack { }, }); + const callbackUrls = [`${props.webappUrl}/`]; const userPoolClientName = generateResourceName('userpool-client'); this.userPoolClient = this.userPool.addClient(userPoolClientName, { userPoolClientName, @@ -98,8 +103,8 @@ export class AuthStack extends Stack { authorizationCodeGrant: true, }, scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE], - //callbackUrls, - //logoutUrls: callbackUrls, + callbackUrls, + logoutUrls: callbackUrls, }, accessTokenValidity: Duration.minutes(60), idTokenValidity: Duration.minutes(60), diff --git a/cloud/lib/index.ts b/cloud/lib/index.ts index 7e4b6f799..0427fcf54 100644 --- a/cloud/lib/index.ts +++ b/cloud/lib/index.ts @@ -1,3 +1,4 @@ export * from './resourceNamingUtils'; export { ApiStack } from './api-stack'; export { AuthStack } from './auth-stack'; +export { UiStack } from './ui-stack'; diff --git a/cloud/lib/ui-stack.ts b/cloud/lib/ui-stack.ts new file mode 100644 index 000000000..1a7667d61 --- /dev/null +++ b/cloud/lib/ui-stack.ts @@ -0,0 +1,102 @@ +import { + AllowedMethods, + CacheCookieBehavior, + CachePolicy, + Distribution, + OriginAccessIdentity, + SecurityPolicyProtocol, + ViewerProtocolPolicy, +} from 'aws-cdk-lib/aws-cloudfront'; +import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'; +import { + CfnOutput, + Duration, + RemovalPolicy, + Stack, + StackProps, +} from 'aws-cdk-lib/core'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { + BlockPublicAccess, + Bucket, + BucketEncryption, +} from 'aws-cdk-lib/aws-s3'; +import { Construct } from 'constructs'; + +import { resourceName } from './resourceNamingUtils'; + +export class UiStack extends Stack { + public readonly cloudfrontUrl: string; + + constructor(scope: Construct, id: string, props: StackProps) { + super(scope, id, props); + + const generateResourceName = resourceName(scope); + + // allow s3 to be secured + const cloudfrontOAI = new OriginAccessIdentity( + this, + generateResourceName('cloudfront-OAI') + ); + + //HostBucket + const bucketName = generateResourceName('host-bucket'); + const hostBucket = new Bucket(this, bucketName, { + bucketName, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + encryption: BucketEncryption.S3_MANAGED, + versioned: false, + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, + }); + hostBucket.addToResourcePolicy( + new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: [hostBucket.arnForObjects('*')], + principals: [ + new iam.CanonicalUserPrincipal( + cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId + ), + ], + }) + ); + + //CloudFront + const cachePolicyName = generateResourceName('site-cache-policy'); + const cloudFront = new Distribution( + this, + generateResourceName('site-distribution'), + { + defaultRootObject: 'index.html', + minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, + errorResponses: [ + { + httpStatus: 404, + responseHttpStatus: 200, + responsePagePath: '/index.html', + ttl: Duration.seconds(30), + }, + ], + defaultBehavior: { + origin: new S3Origin(hostBucket, { + originAccessIdentity: cloudfrontOAI, + }), + cachePolicy: new CachePolicy(this, cachePolicyName, { + cachePolicyName, + cookieBehavior: CacheCookieBehavior.allowList( + 'prompt-injection.sid' + ), + }), + compress: true, + allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS, + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + }, + } + ); + this.cloudfrontUrl = `https://${cloudFront.domainName}`; + + new CfnOutput(this, 'WebURL', { + value: this.cloudfrontUrl, + }); + } +}