From dc541d3ce701c69ffcfbca9ca9c4bf1af0ef51e0 Mon Sep 17 00:00:00 2001
From: Stefan Pandzic <stefan.pandzic>
Date: Thu, 12 Dec 2024 15:48:43 +0100
Subject: [PATCH] added cloud front for images

---
 backendAWS/bin/backend_aws.ts                | 12 +--
 backendAWS/functions/create-contact/index.ts |  3 +-
 backendAWS/functions/update-contact/index.ts |  4 +-
 backendAWS/lib/cloudfront_stack.ts           | 80 --------------------
 backendAWS/lib/frontend_stack.ts             | 12 ++-
 backendAWS/lib/lambda_stack.ts               | 13 +++-
 backendAWS/lib/storage_stack.ts              | 40 ++++++++++
 frontend/src/constants/index.ts              |  2 +-
 8 files changed, 70 insertions(+), 96 deletions(-)
 delete mode 100644 backendAWS/lib/cloudfront_stack.ts

diff --git a/backendAWS/bin/backend_aws.ts b/backendAWS/bin/backend_aws.ts
index 62db5f8..be02dd4 100644
--- a/backendAWS/bin/backend_aws.ts
+++ b/backendAWS/bin/backend_aws.ts
@@ -27,9 +27,11 @@ const storageStack = new StorageStack(app, 'ContactApp-StorageStack', {
     region: process.env.CDK_DEFAULT_REGION,
   },
 });
+
 const lambdaStack = new LambdaStack(app, 'ContactApp-LambdaStack', {
   contactsTable: storageStack.contactsTable,
   contactsBucket: storageStack.contactsBucket,
+  cloudfrontImagesDistribution: storageStack.cloudfrontImagesDistribution,
   env: {
     account: process.env.CDK_DEFAULT_ACCOUNT,
     region: process.env.CDK_DEFAULT_REGION,
@@ -48,16 +50,8 @@ new ApiGatewayStack(app, 'ContactApp-ApiGatewayStack', {
   },
 });
 
-/*  new CloudFrontStack(app, 'ContactApp-CloudFrontStack', {
-  contactsBucket: storageStack.contactsBucket,
-  env: {
-    account: process.env.CDK_DEFAULT_ACCOUNT,
-    region: process.env.CDK_DEFAULT_REGION,
-  }, 
-});
- */
-
 new FrontendStack(app, 'ContactApp-FrontendStack', {
+  contactsBucket: storageStack.contactsBucket,
   env: {
     account: process.env.CDK_DEFAULT_ACCOUNT,
     region: process.env.CDK_DEFAULT_REGION,
diff --git a/backendAWS/functions/create-contact/index.ts b/backendAWS/functions/create-contact/index.ts
index 002eef4..bbea234 100644
--- a/backendAWS/functions/create-contact/index.ts
+++ b/backendAWS/functions/create-contact/index.ts
@@ -6,6 +6,7 @@ const dynamoDb = new DynamoDB.DocumentClient();
 const s3 = new S3();
 const tableName = process.env.TABLE_NAME;
 const bucketName = process.env.BUCKET_NAME;
+const cloudfrontDomain = process.env.CLOUDFRONT_DOMAIN;
 
 export const handler = async (event: any) => {
   const body = JSON.parse(event.body);
@@ -40,7 +41,7 @@ export const handler = async (event: any) => {
       phone: body.phone,
       email: body.email,
       isFavorite: body.isFavorite || false,
-      imageUrl: `https://${bucketName}.s3.amazonaws.com/${imageKey}`,
+      imageUrl: `https://${cloudfrontDomain}/${imageKey}?timestamp=${Date.now()}`,
     };
 
     // Store contact in DynamoDB
diff --git a/backendAWS/functions/update-contact/index.ts b/backendAWS/functions/update-contact/index.ts
index 8735500..f117d2a 100644
--- a/backendAWS/functions/update-contact/index.ts
+++ b/backendAWS/functions/update-contact/index.ts
@@ -5,6 +5,7 @@ const dynamoDb = new DynamoDB.DocumentClient();
 const s3 = new S3();
 const tableName = process.env.TABLE_NAME;
 const bucketName = process.env.BUCKET_NAME;
+const cloudfrontDomain = process.env.CLOUDFRONT_DOMAIN;
 
 export const handler = async (event: any) => {
   const body = JSON.parse(event.body);
@@ -42,10 +43,11 @@ export const handler = async (event: any) => {
           Key: imageKey,
           Body: imageBuffer,
           ContentType: 'image/jpeg',
+          CacheControl: 'max-age=0, must-revalidate',
         })
         .promise();
 
-      imageUrl = `https://${bucketName}.s3.amazonaws.com/${imageKey}`;
+      imageUrl = `https://${cloudfrontDomain}/${imageKey}?timestamp=${Date.now()}`;
     }
 
     // Update the contact in DynamoDB
diff --git a/backendAWS/lib/cloudfront_stack.ts b/backendAWS/lib/cloudfront_stack.ts
deleted file mode 100644
index 3b64122..0000000
--- a/backendAWS/lib/cloudfront_stack.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import * as cdk from 'aws-cdk-lib';
-import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
-import * as s3 from 'aws-cdk-lib/aws-s3';
-import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
-
-interface CloudFrontStackProps extends cdk.StackProps {
-  contactsBucket: s3.Bucket;
-}
-
-export class CloudFrontStack extends cdk.Stack {
-  constructor(scope: cdk.App, id: string, props: CloudFrontStackProps) {
-    super(scope, id, props);
-
-    // Create an Origin Access Control (OAC)
-    const originAccessControl = new cloudfront.CfnOriginAccessControl(
-      this,
-      'ContactsAppOAC',
-      {
-        originAccessControlConfig: {
-          name: 'ContactsAppOAC',
-          originAccessControlOriginType: 's3',
-          signingBehavior: 'always', // Always sign requests
-          signingProtocol: 'sigv4',
-        },
-      }
-    );
-
-    //CloudFront Distribution for Images
-    /* 
-    const cloudFrontDistribution = new cloudfront.CfnDistribution(
-      this,
-      'ContactsAppDistribution',
-      {
-        distributionConfig: {
-          enabled: true,
-          defaultRootObject: '', // Optional: Redirect root paths to index.html
-          origins: [
-            {
-              id: 'S3Origin',
-              domainName: props.contactsBucket.bucketRegionalDomainName, // Use the regional domain name of the S3 bucket
-              originAccessControlId: originAccessControl.attrId, // Attach the OAC
-              s3OriginConfig: {}, // Required when using S3 as the origin
-            },
-          ],
-          defaultCacheBehavior: {
-            targetOriginId: 'S3Origin',
-            viewerProtocolPolicy: 'redirect-to-https', // Enforce HTTPS
-            allowedMethods: ['GET', 'HEAD'],
-            cachedMethods: ['GET', 'HEAD'],
-            forwardedValues: {
-              queryString: false,
-              cookies: { forward: 'none' },
-            },
-          },
-          priceClass: 'PriceClass_100', // Adjust to your requirements
-          viewerCertificate: {
-            cloudFrontDefaultCertificate: true,
-          },
-        },
-      }
-    );
-
-    props.contactsBucket.addToResourcePolicy(
-      new PolicyStatement({
-        actions: ['s3:GetObject'],
-        resources: [`${props.contactsBucket.bucketArn}/*`],
-        principals: [
-          new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com'),
-        ],
-        conditions: {
-          StringEquals: {
-            'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${cloudFrontDistribution.ref}`,
-          },
-        },
-      })
-    );
-
-    cdk.Tags.of(cloudFrontDistribution).add('Service', 'CloudFront'); */
-  }
-}
diff --git a/backendAWS/lib/frontend_stack.ts b/backendAWS/lib/frontend_stack.ts
index 2ffc139..e05fcc0 100644
--- a/backendAWS/lib/frontend_stack.ts
+++ b/backendAWS/lib/frontend_stack.ts
@@ -5,8 +5,14 @@ import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
 import { S3StaticWebsiteOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
 import { join } from 'path';
 
+interface FrontendStackProps extends cdk.StackProps {
+  contactsBucket: s3.Bucket;
+}
+
 export class FrontendStack extends cdk.Stack {
-  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
+  public readonly cloudfrontDistribution: cloudfront.Distribution;
+
+  constructor(scope: cdk.App, id: string, props: FrontendStackProps) {
     super(scope, id, props);
 
     // Create an S3 bucket for the frontend
@@ -39,7 +45,7 @@ export class FrontendStack extends cdk.Stack {
     });
 
     // Create CloudFront distribution
-    const cloudfrontDistribution = new cloudfront.Distribution(
+    this.cloudfrontDistribution = new cloudfront.Distribution(
       this,
       'FrontendDistribution',
       {
@@ -53,7 +59,7 @@ export class FrontendStack extends cdk.Stack {
 
     // Output the CloudFront URL
     new cdk.CfnOutput(this, 'CloudFrontClientAppURL', {
-      value: `https://${cloudfrontDistribution.distributionDomainName}`,
+      value: `https://${this.cloudfrontDistribution.distributionDomainName}`,
       description: 'CloudFront URL for frontend',
     });
   }
diff --git a/backendAWS/lib/lambda_stack.ts b/backendAWS/lib/lambda_stack.ts
index f9d5917..6bdb031 100644
--- a/backendAWS/lib/lambda_stack.ts
+++ b/backendAWS/lib/lambda_stack.ts
@@ -3,12 +3,14 @@ import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
 import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
 import * as s3 from 'aws-cdk-lib/aws-s3';
+import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
 import * as path from 'path';
 import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
 
 interface LambdaStackProps extends cdk.StackProps {
   contactsTable: dynamodb.Table;
   contactsBucket: s3.Bucket;
+  cloudfrontImagesDistribution: cloudfront.Distribution;
 }
 
 export class LambdaStack extends cdk.Stack {
@@ -20,7 +22,8 @@ export class LambdaStack extends cdk.Stack {
   constructor(scope: cdk.App, id: string, props: LambdaStackProps) {
     super(scope, id, props);
 
-    const { contactsTable, contactsBucket } = props;
+    const { contactsTable, contactsBucket, cloudfrontImagesDistribution } =
+      props;
 
     //Create Contact Function Lambda
     this.createContactFunction = new lambdaNodejs.NodejsFunction(
@@ -34,6 +37,8 @@ export class LambdaStack extends cdk.Stack {
         environment: {
           TABLE_NAME: contactsTable.tableName,
           BUCKET_NAME: contactsBucket.bucketName,
+          CLOUDFRONT_DOMAIN:
+            cloudfrontImagesDistribution.distributionDomainName,
         },
       }
     );
@@ -64,6 +69,8 @@ export class LambdaStack extends cdk.Stack {
         handler: 'handler',
         environment: {
           TABLE_NAME: contactsTable.tableName,
+          CLOUDFRONT_DOMAIN:
+            cloudfrontImagesDistribution.distributionDomainName,
         },
       }
     );
@@ -90,6 +97,8 @@ export class LambdaStack extends cdk.Stack {
         environment: {
           TABLE_NAME: contactsTable.tableName,
           BUCKET_NAME: contactsBucket.bucketName,
+          CLOUDFRONT_DOMAIN:
+            cloudfrontImagesDistribution.distributionDomainName,
         },
       }
     );
@@ -121,6 +130,8 @@ export class LambdaStack extends cdk.Stack {
         environment: {
           TABLE_NAME: contactsTable.tableName,
           BUCKET_NAME: contactsBucket.bucketName,
+          CLOUDFRONT_DOMAIN:
+            cloudfrontImagesDistribution.distributionDomainName,
         },
       }
     );
diff --git a/backendAWS/lib/storage_stack.ts b/backendAWS/lib/storage_stack.ts
index 18db14c..8dbc003 100644
--- a/backendAWS/lib/storage_stack.ts
+++ b/backendAWS/lib/storage_stack.ts
@@ -1,10 +1,13 @@
 import * as cdk from 'aws-cdk-lib';
 import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
+import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
 import * as s3 from 'aws-cdk-lib/aws-s3';
+import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
 
 export class StorageStack extends cdk.Stack {
   public readonly contactsTable: dynamodb.Table;
   public readonly contactsBucket: s3.Bucket;
+  public readonly cloudfrontImagesDistribution: cloudfront.Distribution;
 
   constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
     super(scope, id, props);
@@ -27,6 +30,43 @@ export class StorageStack extends cdk.Stack {
       blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
     });
 
+    // Create CloudFront for Images distribution
+    this.cloudfrontImagesDistribution = new cloudfront.Distribution(
+      this,
+      'CloudfrontImagesDistribution',
+      {
+        defaultBehavior: {
+          origin: origins.S3BucketOrigin.withOriginAccessControl(
+            this.contactsBucket
+          ),
+          viewerProtocolPolicy:
+            cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, // Redirect HTTP to HTTPS
+          cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, // Adjust Cache Policy
+        },
+      }
+    );
+
+    this.contactsBucket.addToResourcePolicy(
+      new cdk.aws_iam.PolicyStatement({
+        actions: ['s3:GetObject'],
+        resources: [`${this.contactsBucket.bucketArn}/*`],
+        principals: [
+          new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com'),
+        ],
+        conditions: {
+          StringEquals: {
+            'AWS:SourceArn': `arn:aws:cloudfront::${
+              cdk.Stack.of(this).account
+            }:distribution/${this.cloudfrontImagesDistribution.distributionId}`,
+          },
+        },
+      })
+    );
+
     cdk.Tags.of(this.contactsBucket).add('Service', 'Storage');
+    cdk.Tags.of(this.cloudfrontImagesDistribution).add(
+      'Service',
+      'CloudFrontImagesDistribution'
+    );
   }
 }
diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts
index 9fa51bf..d574f09 100644
--- a/frontend/src/constants/index.ts
+++ b/frontend/src/constants/index.ts
@@ -6,4 +6,4 @@ export const poolData = {
   ClientId: '42nc47ndifaeqe861c16eaj30s',
 };
 export const baseURL =
-  'https://yiwft1rh0l.execute-api.eu-north-1.amazonaws.com/prod/';
+  'https://enl05xtmpb.execute-api.eu-north-1.amazonaws.com/prod/';
-- 
GitLab