├── functions ├── updateUser.js ├── deleteEstimate.js ├── deleteLocation.js ├── deleteUser.js ├── getAllBookings.js ├── getAllLocations.js ├── getAllUsers.js ├── updateBooking.js ├── updateLocation.js ├── getAllEstimates.js ├── getOneEstimate.js ├── getOneLocation.js ├── getOneUser.js ├── createLocation.js ├── createBooking.js ├── validateUser.js ├── createUser.js ├── createEstimate.js ├── getOneLocationByUserId.js └── updateEstimate.js ├── .npmignore ├── jest.config.js ├── test └── rapidcleaners-api-cdk.test.ts ├── package.json ├── tsconfig.json ├── bin └── rapidcleaners-api-cdk.ts ├── README.md ├── cdk.json ├── .gitignore └── lib └── rapidcleaners-api-cdk-stack.ts /functions/updateUser.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/deleteEstimate.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/deleteLocation.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/deleteUser.js: -------------------------------------------------------------------------------- 1 | d -------------------------------------------------------------------------------- /functions/getAllBookings.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/getAllLocations.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/getAllUsers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/updateBooking.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/updateLocation.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/rapidcleaners-api-cdk.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as RapidcleanersApiCdk from '../lib/rapidcleaners-api-cdk-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/rapidcleaners-api-cdk-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new RapidcleanersApiCdk.RapidcleanersApiCdkStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rapidcleaners-api-cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "rapidcleaners-api-cdk": "bin/rapidcleaners-api-cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.12", 15 | "@types/node": "20.12.7", 16 | "aws-cdk": "2.139.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.2", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.4.5" 21 | }, 22 | "dependencies": { 23 | "@aws-cdk/aws-apigateway": "^1.38.0", 24 | "@aws-cdk/aws-cloudwatch": "^1.149.0", 25 | "@aws-cdk/aws-dynamodb": "^1.149.0", 26 | "@aws-cdk/aws-lambda": "^1.19.0", 27 | "aws-cdk-lib": "^2.155.0", 28 | "constructs": "^10.3.0", 29 | "source-map-support": "^0.5.21" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": false, 21 | "inlineSourceMap": true, 22 | "inlineSources": true, 23 | "experimentalDecorators": true, 24 | "strictPropertyInitialization": false, 25 | "typeRoots": [ 26 | "./node_modules/@types" 27 | ] 28 | }, 29 | "include": ["**/*.ts"], 30 | "exclude": [ 31 | "node_modules", 32 | "cdk.out" 33 | ] 34 | } 35 | 36 | -------------------------------------------------------------------------------- /bin/rapidcleaners-api-cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { RapidcleanersApiCdkStack } from '../lib/rapidcleaners-api-cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | new RapidcleanersApiCdkStack(app, 'RapidcleanersApiCdkStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /functions/getAllEstimates.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 3 | 4 | // Environment variable for the table name 5 | const TABLE_NAME = process.env.TABLE_NAME; 6 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 7 | 8 | 9 | exports.handler = async (event) => { 10 | try { 11 | // Set up the DynamoDB scan parameters 12 | const params = { 13 | TableName: TABLE_NAME 14 | }; 15 | 16 | // Scan the DynamoDB table to get all items 17 | const data = await dynamoDb.scan(params).promise(); 18 | 19 | // Return the scanned items as the response 20 | return { 21 | statusCode: 200, 22 | body: JSON.stringify({ estimates: data.Items }), 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, // Enable CORS 26 | 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 27 | } 28 | }; 29 | } catch (error) { 30 | console.error('Error retrieving estimates:', error); 31 | 32 | // Return an error response if something goes wrong 33 | return { 34 | statusCode: 500, 35 | body: JSON.stringify({ message: 'Internal Server Error', error: error.message }), 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, // Enable CORS 39 | 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 40 | } 41 | }; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RapidCleaners API 2 | ## AWS IaC Project (Infra As Code) 3 | 4 | This project was deployed using 5 | ``` zsrc 6 | cdk init -a app --language=typescript 7 | ``` 8 | 9 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 10 | 11 | ## Prereqs 12 | -aws-cli 13 | 14 | -aws-cdk 15 | 16 | -nvm [ensure correct version of node] 17 | 18 | `Nvm install 20.11.0` 19 | 20 | `Nvm use 20.11.0` 21 | 22 | `nvm install-latest-npm` 23 | 24 | -npm [installs npx since 5.0] 25 | 26 | 27 | ## CLone and Code 28 | -clone repository 29 | 30 | -cd into repository 31 | 32 | -run `npm install` 33 | 34 | if necessary, install them individually from `package.json` 35 | 36 | `"@aws-cdk/aws-apigateway": "^1.204.0",` 37 | 38 | `"@aws-cdk/aws-dynamodb": "^1.204.0",` 39 | 40 | `"@aws-cdk/aws-lambda": "^1.204.0",` 41 | 42 | in the terminal, like so 43 | `npm i @aws-cdk/aws-apigateway` 44 | 45 | 46 | 47 | ## Using an IDE 48 | Be sure to install the plugins for aws and aws-cli as well as sam and aws-cdk-lib 49 | 50 | whether you are using terminal and CLI or an IDK with the AWS SDK 51 | run locally and/or deploy to aws 52 | 53 | ### Setting the env ['dev', 'stage', 'prod'] 54 | ``` 55 | let environment = 'stage'; // Adjust this manually as needed 56 | ``` 57 | 58 | 59 | ## Useful commands 60 | 61 | * `npm run build` compile typescript to js 62 | * `npm run watch` watch for changes and compile 63 | * `npm run test` perform the jest unit tests 64 | 65 | * `cdk bootstap` allows you to authenticate if you don't already have a .aws/credentials 66 | * `cdk diff` compare deployed stack with current state 67 | * `cdk synth` emits the synthesized CloudFormation template 68 | * `cdk deploy` deploy this stack to your default AWS account/region 69 | 70 | 71 | * `cdk destroy` destroy this stack to your default AWS account/region 72 | 73 | 74 | export ENVIRONMENT=dev 75 | cdk synth 76 | cdk deploy 77 | 78 | export ENVIRONMENT=stage 79 | cdk synth 80 | cdk deploy 81 | 82 | export ENVIRONMENT=prod 83 | cdk synth 84 | cdk deploy 85 | 86 | 87 | -------------------------------------------------------------------------------- /functions/getOneEstimate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const AWS = require("aws-sdk"); 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | const TABLE_NAME = process.env.TABLE_NAME; // Ensure the table name is set in the environment variables 5 | const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 6 | 7 | exports.handler = async (event) => { 8 | const estimateId = event.pathParameters.estimateId; // Extract EstimateId from the request path 9 | 10 | try { 11 | console.log(`[getOneEstimate] estimateId: ${estimateId}`); 12 | 13 | // Validate if estimateId is provided 14 | if (!estimateId) { 15 | return { 16 | statusCode: 400, 17 | headers: { 18 | "Content-Type": "application/json", 19 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 20 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 21 | }, 22 | body: JSON.stringify({ message: '"estimateId" is required' }), 23 | }; 24 | } 25 | 26 | // Define the parameters to query DynamoDB 27 | const params = { 28 | TableName: TABLE_NAME, 29 | Key: { estimateId }, 30 | }; 31 | 32 | // Get the estimate from DynamoDB 33 | const result = await dynamoDb.get(params).promise(); 34 | 35 | if (!result.Item) { 36 | return { 37 | statusCode: 404, 38 | headers: { 39 | 40 | "Content-Type": "application/json", 41 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 42 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 43 | }, 44 | body: JSON.stringify({ message: 'Estimate not found' }), 45 | }; 46 | } 47 | 48 | // Return the retrieved estimate 49 | return { 50 | statusCode: 200, 51 | headers: { 52 | "Content-Type": "application/json", 53 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 54 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 55 | }, 56 | body: JSON.stringify(result.Item), 57 | }; 58 | 59 | } catch (error) { 60 | console.error(`[getOneEstimate] Error retrieving estimate: ${error.message}`); 61 | return { 62 | statusCode: 500, 63 | headers: { 64 | "Content-Type": "application/json", 65 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 66 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 67 | }, 68 | body: JSON.stringify({ message: 'Internal Server Error', error: error.message }), 69 | }; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /functions/getOneLocation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AWS = require("aws-sdk"); 4 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 | var TABLE_NAME = process.env.TABLE_NAME; // Ensure the table name for locations is set in the environment variables 6 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 7 | 8 | c 9 | exports.handler = async (event) => { 10 | console.log("[getOneLocation] Received event:", JSON.stringify(event, null, 2)); 11 | 12 | try { 13 | const locationId = event.pathParameters.locationId; // Assuming the locationId is passed via path parameters 14 | 15 | // Validate the locationId 16 | if (!locationId || typeof locationId !== "string") { 17 | return { 18 | statusCode: 400, 19 | headers: { 20 | "Content-Type": "application/json", 21 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 22 | "Access-Control-Allow-Methods": "GET,OPTIONS" 23 | }, 24 | body: JSON.stringify({ message: 'Invalid request: "locationId" is required and must be a string.' }), 25 | }; 26 | } 27 | 28 | // Define the params for the DynamoDB get operation 29 | const params = { 30 | TableName: TABLE_NAME, 31 | Key: { locationId: locationId } 32 | }; 33 | 34 | // Perform the get operation from DynamoDB 35 | const data = await dynamoDb.get(params).promise(); 36 | 37 | if (!data.Item) { 38 | return { 39 | statusCode: 404, 40 | headers: { 41 | "Content-Type": "application/json", 42 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 43 | "Access-Control-Allow-Methods": "GET,OPTIONS" 44 | }, 45 | body: JSON.stringify({ message: "Location not found." }), 46 | }; 47 | } 48 | 49 | // Return the found location 50 | return { 51 | statusCode: 200, 52 | headers: { 53 | "Content-Type": "application/json", 54 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 55 | "Access-Control-Allow-Methods": "GET,OPTIONS" 56 | }, 57 | body: JSON.stringify({ location: data.Item }), 58 | }; 59 | 60 | } catch (error) { 61 | console.error("Error retrieving location:", error); 62 | return { 63 | statusCode: 500, 64 | headers: { 65 | "Content-Type": "application/json", 66 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 67 | "Access-Control-Allow-Methods": "GET,OPTIONS" 68 | }, 69 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 70 | }; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /functions/getOneUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AWS = require("aws-sdk"); 4 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 | var TABLE_NAME = process.env.TABLE_NAME; // Ensure the table name is set in the Lambda environment 6 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 7 | 8 | 9 | exports.handler = async (event) => { 10 | console.log("[getOneUser] Received event:", JSON.stringify(event, null, 2)); 11 | 12 | try { 13 | const userId = event.pathParameters.userId; // Assuming the userId is passed via path parameters 14 | 15 | // Validate the userId 16 | if (!userId || typeof userId !== "string") { 17 | return { 18 | statusCode: 400, 19 | headers: { 20 | "Content-Type": "application/json", 21 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 22 | "Access-Control-Allow-Methods": "GET,OPTIONS" 23 | }, 24 | body: JSON.stringify({ message: 'Invalid request: "userId" is required and must be a string.' }), 25 | }; 26 | } 27 | 28 | // Define the params for the DynamoDB get operation 29 | const params = { 30 | TableName: TABLE_NAME, 31 | Key: { userId: userId } // The userId is the partition key 32 | }; 33 | 34 | // Perform the get operation from DynamoDB 35 | const data = await dynamoDb.get(params).promise(); 36 | 37 | // Check if the user was found 38 | if (!data.Item) { 39 | return { 40 | statusCode: 404, 41 | headers: { 42 | "Content-Type": "application/json", 43 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 44 | "Access-Control-Allow-Methods": "GET,OPTIONS" 45 | }, 46 | body: JSON.stringify({ message: "User not found." }), 47 | }; 48 | } 49 | 50 | // Return the found user 51 | return { 52 | statusCode: 200, 53 | headers: { 54 | "Content-Type": "application/json", 55 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 56 | "Access-Control-Allow-Methods": "GET,OPTIONS" 57 | }, 58 | body: JSON.stringify({ user: data.Item }), 59 | }; 60 | 61 | } catch (error) { 62 | console.error("Error retrieving user:", error); 63 | return { 64 | statusCode: 500, 65 | headers: { 66 | "Content-Type": "application/json", 67 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 68 | "Access-Control-Allow-Methods": "GET,OPTIONS" 69 | }, 70 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 71 | }; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /functions/createLocation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AWS = require("aws-sdk"); 4 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 | var TABLE_NAME = process.env.TABLE_NAME; // Ensure the table name for locations is set in the environment variables 6 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 7 | 8 | exports.handler = async (event) => { 9 | console.log("[createLocation] Received event:", JSON.stringify(event, null, 2)); 10 | 11 | try { 12 | const requestBody = JSON.parse(event.body); 13 | console.log('[createLocation] requestBody: ', requestBody); 14 | 15 | // Validate the location details 16 | if (!requestBody.locationdetails || typeof requestBody.locationdetails !== "object") { 17 | return { 18 | statusCode: 400, 19 | headers: { 20 | "Content-Type": "application/json", 21 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 22 | "Access-Control-Allow-Methods": "POST,OPTIONS" 23 | }, 24 | body: JSON.stringify({ message: '"locationdetails" is required and must be an object.' }), 25 | }; 26 | } 27 | 28 | const locationId = requestBody.locationId; 29 | if (!locationId || typeof locationId !== "string") { 30 | return { 31 | statusCode: 400, 32 | headers: { 33 | "Content-Type": "application/json", 34 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 35 | "Access-Control-Allow-Methods": "POST,OPTIONS" 36 | }, 37 | body: JSON.stringify({ message: '"locationId" is required and must be a string.' }), 38 | }; 39 | } 40 | 41 | // Define the params for the DynamoDB put operation 42 | const params = { 43 | TableName: TABLE_NAME, 44 | Item: { 45 | locationId: locationId, 46 | locationdetails: requestBody.locationdetails 47 | } 48 | }; 49 | 50 | // Perform the put operation in DynamoDB 51 | await dynamoDb.put(params).promise(); 52 | console.log('[createLocation] params: ', params); 53 | 54 | // Return a successful response 55 | return { 56 | statusCode: 201, 57 | headers: { 58 | "Content-Type": "application/json", 59 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 60 | "Access-Control-Allow-Methods": "POST,OPTIONS" 61 | }, 62 | body: JSON.stringify({ message: "Location created successfully!", item: params.Item }), 63 | }; 64 | 65 | } catch (error) { 66 | console.error("Error creating location:", error); 67 | return { 68 | statusCode: 500, 69 | headers: { 70 | "Content-Type": "application/json", 71 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 72 | "Access-Control-Allow-Methods": "POST,OPTIONS" 73 | }, 74 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 75 | }; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /functions/createBooking.js: -------------------------------------------------------------------------------- 1 | // BookingCreate Lambda function 2 | const AWS = require('aws-sdk'); 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | const TABLE_NAME = process.env.TABLE_NAME; 5 | const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 6 | 7 | exports.handler = async (event) => { 8 | try { 9 | console.log("[createBooking] Received event:", JSON.stringify(event, null, 2)); 10 | 11 | const requestBody = JSON.parse(event.body); 12 | console.log('[createBooking] requestBody:', requestBody); 13 | 14 | if (!requestBody.bookingId || !requestBody.bookingDetails) { 15 | return { 16 | statusCode: 400, 17 | headers: { 18 | "Content-Type": "application/json", 19 | "Access-Control-Allow-Methods": "POST,OPTIONS", 20 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN 21 | }, 22 | body: JSON.stringify({ message: 'Invalid request: "bookingId" and "bookingDetails" are required.' }), 23 | }; 24 | } 25 | const bookingId = requestBody.bookingId; 26 | const bookingDetails = requestBody.bookingDetails; 27 | // const estimateId = requestBody.bookingDetails.estimateId; 28 | // const locationId = requestBody.bookingDetails.locationId; 29 | // const userId = requestBody.bookingDetails.userId; 30 | // const name = requestBody.bookingDetails.name || "unknown"; 31 | // const email = requestBody.bookingDetails.email || "unknown"; 32 | // const phone = requestBody.bookingDetails.phone || "unknown"; 33 | // const location = requestBody.bookingDetails.location || "unknown address"; 34 | // const start = requestBody.bookingDetails.start; 35 | // const end = requestBody.bookingDetails.end; 36 | // const duration = requestBody.bookingDetails.duration || 0; 37 | // const status = requestBody.status || "pending"; 38 | // const eventTitle = requestBody.bookingDetails.eventTitle || "N/A"; 39 | 40 | 41 | const params = { 42 | TableName: TABLE_NAME, 43 | Item: { 44 | bookingId, 45 | bookingDetails, 46 | }, 47 | }; 48 | 49 | 50 | await dynamoDb.put(params).promise(); 51 | 52 | return { 53 | statusCode: 200, 54 | headers: { 55 | "Content-Type": "application/json", 56 | "Access-Control-Allow-Methods": "POST,OPTIONS", 57 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN 58 | 59 | }, 60 | body: JSON.stringify({ message: 'Booking created successfully', item: params.Item }), 61 | }; 62 | } catch (error) { 63 | console.error('Error creating booking: ', error); 64 | return { 65 | statusCode: 500, 66 | headers: { 67 | "Content-Type": "application/json", 68 | "Access-Control-Allow-Methods": "POST,OPTIONS", 69 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN 70 | }, 71 | body: JSON.stringify({ message: 'Error creating booking', error }), 72 | }; 73 | } 74 | }; 75 | 76 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/rapidcleaners-api-cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 63 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 64 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 65 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 66 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 67 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /functions/validateUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AWS = require("aws-sdk"); 4 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 | var TABLE_NAME = process.env.TABLE_NAME; 6 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; 7 | 8 | exports.handler = async (event) => { 9 | try { 10 | const body = JSON.parse(event.body); 11 | console.log('[validateUser] event.body:', event.body); 12 | 13 | const { estimateId, userId } = body; 14 | 15 | console.log('[validateUser] estimateId from event.body:', estimateId); 16 | console.log('[validateUser] userId from event.body:', userId); 17 | 18 | // Validate input 19 | if (!estimateId) { 20 | console.log('[validateUser] Missing estimateId') 21 | return { 22 | statusCode: 400, 23 | headers: { 24 | "Content-Type": "application/json", 25 | "Access-Control-Allow-Methods": "POST,OPTIONS", 26 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 27 | }, 28 | body: JSON.stringify({ message: "Missing estimateId." }) 29 | }; 30 | } 31 | 32 | // Query the estimate by estimateId 33 | const params = { 34 | TableName: TABLE_NAME, 35 | Key: { 36 | estimateId: estimateId, // Query by estimateId only 37 | } 38 | }; 39 | 40 | const result = await dynamoDb.get(params).promise(); 41 | 42 | console.log('[validateUser] result:', result); 43 | console.log('[validateUser] result.Item:', result.Item); 44 | console.log('[validateUser] result.Item.servicedetails:', result.Item.servicedetails); 45 | 46 | 47 | if (!result.Item) { 48 | return { 49 | statusCode: 404, 50 | headers: { 51 | "Content-Type": "application/json", 52 | "Access-Control-Allow-Methods": "POST,OPTIONS", 53 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 54 | }, 55 | body: JSON.stringify({ message: "Estimate not found." }) 56 | }; 57 | } 58 | 59 | const estimate = result.Item; 60 | 61 | console.log('[validateUser] estimate.servicedetails.userID:', estimate.servicedetails.userID); 62 | 63 | // Compare userId from the request with userID in the estimate's servicedetails 64 | if (estimate.servicedetails.userID === userId) { 65 | return { 66 | statusCode: 200, 67 | headers: { 68 | "Content-Type": "application/json", 69 | "Access-Control-Allow-Methods": "POST,OPTIONS", 70 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 71 | }, 72 | body: JSON.stringify({ message: "User authenticated successfully.", estimate: estimate }) 73 | }; 74 | } else { 75 | return { 76 | statusCode: 403, 77 | headers: { 78 | "Content-Type": "application/json", 79 | "Access-Control-Allow-Methods": "POST,OPTIONS", 80 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 81 | }, 82 | body: JSON.stringify({ message: "UserID does not match the estimate." }) 83 | }; 84 | } 85 | 86 | } catch (error) { 87 | console.error('[validateUser] error:', error.message); 88 | return { 89 | statusCode: 500, 90 | headers: { 91 | "Content-Type": "application/json", 92 | "Access-Control-Allow-Methods": "POST,OPTIONS", 93 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 94 | }, 95 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }) 96 | }; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /functions/createUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AWS = require("aws-sdk"); 4 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 | var TABLE_NAME = process.env.TABLE_NAME; // Make sure to set this table name in the environment variables 6 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 7 | 8 | exports.handler = async (event) => { 9 | console.log("[createUser] Received event:", JSON.stringify(event, null, 2)); 10 | 11 | try { 12 | const requestBody = JSON.parse(event.body); 13 | console.log('[createUser] requestBody:', requestBody); 14 | 15 | // Validate userId and user details 16 | if (!requestBody.userId || typeof requestBody.userId !== "string") { 17 | return { 18 | statusCode: 400, 19 | headers: { 20 | "Content-Type": "application/json", 21 | "Access-Control-Allow-Methods": "POST,OPTIONS", 22 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN 23 | 24 | }, 25 | body: JSON.stringify({ message: 'Invalid request: "userId" is required and must be a string.' }), 26 | }; 27 | } 28 | 29 | if (!requestBody.userDetails || typeof requestBody.userDetails !== "object") { 30 | return { 31 | statusCode: 400, 32 | headers: { 33 | "Content-Type": "application/json", 34 | "Access-Control-Allow-Methods": "POST,OPTIONS", 35 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN 36 | 37 | }, 38 | body: JSON.stringify({ message: 'Invalid request: "userDetails" is required and must be an object.' }), 39 | }; 40 | } 41 | 42 | // Define required fields in userDetails 43 | const requiredFields = ["email", "firstname", "lastname", "phone"]; 44 | for (const field of requiredFields) { 45 | if (!(field in requestBody.userDetails)) { 46 | return { 47 | statusCode: 400, 48 | headers: { 49 | "Content-Type": "application/json", 50 | "Access-Control-Allow-Methods": "POST,OPTIONS", 51 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN 52 | 53 | 54 | }, 55 | body: JSON.stringify({ message: `Invalid request: "userDetails.${field}" is required.` }), 56 | }; 57 | } 58 | } 59 | 60 | // Define the params for the DynamoDB put operation 61 | const params = { 62 | TableName: TABLE_NAME, 63 | Item: { 64 | userId: requestBody.userId, // Store userId 65 | userDetails: requestBody.userDetails, // Store user details 66 | } 67 | }; 68 | 69 | // Perform the put operation in DynamoDB 70 | await dynamoDb.put(params).promise(); 71 | 72 | console.log('[createUser] params:', params); 73 | 74 | // Return a successful response 75 | return { 76 | statusCode: 200, 77 | headers: { 78 | "Content-Type": "application/json", 79 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 80 | "Access-Control-Allow-Methods": "POST,OPTIONS" 81 | }, 82 | body: JSON.stringify({ message: "User created successfully!", item: params.Item }), 83 | }; 84 | 85 | } catch (error) { 86 | console.error("Error creating user:", error); 87 | return { 88 | statusCode: 500, 89 | headers: { 90 | "Content-Type": "application/json", 91 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 92 | "Access-Control-Allow-Methods": "POST,OPTIONS" 93 | }, 94 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 95 | }; 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /functions/createEstimate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var AWS = require("aws-sdk"); 3 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | var TABLE_NAME = process.env.TABLE_NAME; // Read the table name from environment variable 5 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 6 | 7 | exports.handler = async (event) => { 8 | try { 9 | console.log("Received event:", JSON.stringify(event, null, 2)); 10 | const requestBody = JSON.parse(event.body); 11 | console.log('[createEstimate] requestBody ' , requestBody); 12 | // Input validation 13 | if (!requestBody.estimateId || typeof requestBody.estimateId !== "string") { 14 | return { 15 | statusCode: 400, 16 | headers: { 17 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 18 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 19 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 20 | }, 21 | body: JSON.stringify({ message: 'Invalid request: "estimateId" is required and must be a string.' }) 22 | }; 23 | } 24 | 25 | if (!requestBody.servicedetails || typeof requestBody.servicedetails !== "object") { 26 | return { 27 | statusCode: 400, 28 | headers: { 29 | // "Content-Type": "application/json", 30 | // "Access-Control-Allow-Origin": "http://localhost:3000/", // Ensure CORS for error response 31 | // "Access-Control-Allow-Methods": "POST,OPTIONS", 32 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 33 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 34 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 35 | }, 36 | body: JSON.stringify({ message: 'Invalid request: "servicedetails" is required and must be an object.' }) 37 | }; 38 | } 39 | 40 | // Validate the structure of servicedetails 41 | const requiredFields = [ 42 | "userID", 43 | "typeofservice", 44 | "construct", 45 | "sqft", 46 | "numpeople", 47 | "numrooms", 48 | "numbaths", 49 | "numpets", 50 | "cleanfactor" 51 | ]; 52 | 53 | for (const field of requiredFields) { 54 | if (!(field in requestBody.servicedetails)) { 55 | return { 56 | statusCode: 400, 57 | headers: { 58 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 59 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 60 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 61 | }, 62 | body: JSON.stringify({ message: `Invalid request: "servicedetails.${field}" is required.` }) 63 | }; 64 | } 65 | } 66 | 67 | // Prepare the DynamoDB put parameters 68 | const params = { 69 | TableName: TABLE_NAME, 70 | Item: { 71 | estimateId: requestBody.estimateId, 72 | servicedetails: requestBody.servicedetails 73 | } 74 | }; 75 | 76 | // Insert the item into the DynamoDB table 77 | await dynamoDb.put(params).promise(); 78 | 79 | console.log('[createEstimate] params ', params); 80 | console.log('[createEstimate] params.Item ', params.Item); 81 | 82 | // Return a successful response with correct headers 83 | return { 84 | statusCode: 200, 85 | headers: { 86 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 87 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 88 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 89 | }, 90 | body: JSON.stringify({ message: "Estimate created successfully!", item: params.Item }) 91 | }; 92 | 93 | } catch (error) { 94 | //console.error("Error creating estimate error:", error.message); 95 | console.log('[createEstimate] error.message ' , error.message); 96 | 97 | 98 | // Return an error response with CORS headers 99 | return { 100 | statusCode: 500, 101 | headers: { 102 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 103 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 104 | 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 105 | }, 106 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }) 107 | }; 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /functions/getOneLocationByUserId.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const AWS = require("aws-sdk"); 4 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 5 | const TABLE_NAME = process.env.TABLE_NAME; // Ensure this is passed correctly 6 | const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Ensure this is passed correctly 7 | 8 | exports.handler = async (event) => { 9 | console.log("[getOneLocationByUserId] Received event:", JSON.stringify(event, null, 2)); 10 | 11 | try { 12 | const userId = event.pathParameters.userId; // Assuming the userId is passed via path parameters 13 | console.log(`[getOneLocationByUserId] userId: ${userId}`); 14 | 15 | // Validate the userId 16 | if (!userId || typeof userId !== "string") { 17 | return { 18 | statusCode: 400, 19 | headers: { 20 | "Content-Type": "application/json", 21 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 22 | "Access-Control-Allow-Methods": "GET,OPTIONS" 23 | }, 24 | body: JSON.stringify({ message: 'Invalid request: "userId" is required and must be a string.' }), 25 | }; 26 | } 27 | 28 | // Define the params for the DynamoDB scan operation 29 | const params = { 30 | TableName: TABLE_NAME, 31 | FilterExpression: "locationdetails.userId = :userId", // Search within locationdetails for the userId 32 | ExpressionAttributeValues: { 33 | ":userId": userId 34 | } 35 | }; 36 | 37 | // Perform the scan operation from DynamoDB 38 | const data = await dynamoDb.scan(params).promise(); // Using scan because userId is inside locationdetails 39 | console.log("[getOneLocationByUserId] DynamoDB response data:", JSON.stringify(data, null, 2)); 40 | 41 | // Check if any locations were found 42 | if (data.Items.length === 0) { 43 | return { 44 | statusCode: 404, 45 | headers: { 46 | "Content-Type": "application/json", 47 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 48 | "Access-Control-Allow-Methods": "GET,OPTIONS" 49 | }, 50 | body: JSON.stringify({ message: "Location not found for the given userId." }), 51 | }; 52 | } 53 | 54 | // Return the found location(s) 55 | return { 56 | statusCode: 200, 57 | headers: { 58 | "Content-Type": "application/json", 59 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 60 | "Access-Control-Allow-Methods": "GET,OPTIONS" 61 | }, 62 | body: JSON.stringify({ locations: data.Items }), // Return the array of found locations 63 | }; 64 | 65 | } catch (error) { 66 | console.error("[getOneLocationByUserId] Error retrieving location:", error); 67 | return { 68 | statusCode: 500, 69 | headers: { 70 | "Content-Type": "application/json", 71 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 72 | "Access-Control-Allow-Methods": "GET,OPTIONS" 73 | }, 74 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 75 | }; 76 | } 77 | }; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | // "use strict"; 86 | // 87 | // var AWS = require("aws-sdk"); 88 | // var dynamoDb = new AWS.DynamoDB.DocumentClient(); 89 | // var TABLE_NAME = process.env.TABLE_NAME; // Ensure the table name for locations is set in the environment variables 90 | // var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 91 | // 92 | // 93 | // exports.handler = async (event) => { 94 | // console.log("[getOneLocationByUserId] Received event:", JSON.stringify(event, null, 2)); 95 | // 96 | // try { 97 | // const userId = event.pathParameters.userId; // Assuming the userId is passed via path parameters 98 | // console.log(`[getOneLocationByUserId] userId: ${userId}`); 99 | // 100 | // // Validate the userId 101 | // if (!userId || typeof userId !== "string") { 102 | // return { 103 | // statusCode: 400, 104 | // headers: { 105 | // "Content-Type": "application/json", 106 | // "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 107 | // "Access-Control-Allow-Methods": "GET,OPTIONS" 108 | // }, 109 | // body: JSON.stringify({ message: 'Invalid request: "userId" is required and must be a string.' }), 110 | // }; 111 | // } 112 | // 113 | // // Define the params for the DynamoDB query operation 114 | // const params = { 115 | // TableName: TABLE_NAME, 116 | // IndexName: "userId-index", // Make sure you have a Global Secondary Index (GSI) on userId if it's not the primary key 117 | // KeyConditionExpression: "userId = :userId", 118 | // ExpressionAttributeValues: { 119 | // ":userId": userId 120 | // } 121 | // }; 122 | // 123 | // // Perform the query operation from DynamoDB 124 | // const data = await dynamoDb.query(params).promise(); 125 | // console.log("[getOneLocationByUserId] response data:", JSON.stringify(data, null, 2)); 126 | // console.log("[getOneLocationByUserId] data.Items: ", data.Items); 127 | // 128 | // 129 | // if (data.Items.length === 0) { 130 | // return { 131 | // statusCode: 404, 132 | // headers: { 133 | // "Content-Type": "application/json", 134 | // "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 135 | // "Access-Control-Allow-Methods": "GET,OPTIONS" 136 | // }, 137 | // body: JSON.stringify({ message: "Location not found." }), 138 | // }; 139 | // } 140 | // 141 | // // Return the found location(s) 142 | // return { 143 | // statusCode: 200, 144 | // headers: { 145 | // "Content-Type": "application/json", 146 | // "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 147 | // "Access-Control-Allow-Methods": "GET,OPTIONS" 148 | // }, 149 | // body: JSON.stringify({ locations: data.Items }), 150 | // }; 151 | // 152 | // } catch (error) { 153 | // console.error("Error retrieving location:", error); 154 | // return { 155 | // statusCode: 500, 156 | // headers: { 157 | // "Content-Type": "application/json", 158 | // "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 159 | // "Access-Control-Allow-Methods": "GET,OPTIONS" 160 | // }, 161 | // body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 162 | // }; 163 | // } 164 | // }; 165 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | !functions/*.js 4 | *.d.ts 5 | node_modules 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | 11 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode,node 12 | # Edit at https://www.gitignore.io/?templates=osx,linux,python,windows,pycharm,visualstudiocode,node 13 | 14 | ### Linux ### 15 | *~ 16 | 17 | # temporary files which can be created if a process still has a handle open of a deleted file 18 | .fuse_hidden* 19 | 20 | # KDE directory preferences 21 | .directory 22 | 23 | # Linux trash folder which might appear on any partition or disk 24 | .Trash-* 25 | 26 | # .nfs files are created when an open file is removed but is still being accessed 27 | .nfs* 28 | 29 | ### Node ### 30 | # Logs 31 | logs 32 | *.log 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | lerna-debug.log* 37 | 38 | # Diagnostic reports (https://nodejs.org/api/report.html) 39 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 40 | 41 | # Runtime data 42 | pids 43 | *.pid 44 | *.seed 45 | *.pid.lock 46 | 47 | # Directory for instrumented libs generated by jscoverage/JSCover 48 | lib-cov 49 | 50 | # Coverage directory used by tools like istanbul 51 | coverage 52 | *.lcov 53 | 54 | # nyc test coverage 55 | .nyc_output 56 | 57 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 58 | .grunt 59 | 60 | # Bower dependency directory (https://bower.io/) 61 | bower_components 62 | 63 | # node-waf configuration 64 | .lock-wscript 65 | 66 | # Compiled binary addons (https://nodejs.org/api/addons.html) 67 | build/Release 68 | 69 | # Dependency directories 70 | node_modules/ 71 | jspm_packages/ 72 | 73 | # TypeScript v1 declaration files 74 | typings/ 75 | 76 | # TypeScript cache 77 | *.tsbuildinfo 78 | 79 | # Optional npm cache directory 80 | .npm 81 | 82 | # Optional eslint cache 83 | .eslintcache 84 | 85 | # Optional REPL history 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | *.tgz 90 | 91 | # Yarn Integrity file 92 | .yarn-integrity 93 | 94 | # dotenv environment variables file 95 | .env 96 | .env.test 97 | 98 | # parcel-bundler cache (https://parceljs.org/) 99 | .cache 100 | 101 | # next.js build output 102 | .next 103 | 104 | # nuxt.js build output 105 | .nuxt 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | ### OSX ### 120 | # General 121 | .DS_Store 122 | .AppleDouble 123 | .LSOverride 124 | 125 | # Icon must end with two \r 126 | Icon 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | .com.apple.timemachine.donotpresent 139 | 140 | # Directories potentially created on remote AFP share 141 | .AppleDB 142 | .AppleDesktop 143 | Network Trash Folder 144 | Temporary Items 145 | .apdisk 146 | 147 | ### PyCharm ### 148 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 149 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 150 | 151 | # User-specific stuff 152 | .idea/**/workspace.xml 153 | .idea/**/tasks.xml 154 | .idea/**/usage.statistics.xml 155 | .idea/**/dictionaries 156 | .idea/**/shelf 157 | 158 | # Generated files 159 | .idea/**/contentModel.xml 160 | 161 | # Sensitive or high-churn files 162 | .idea/**/dataSources/ 163 | .idea/**/dataSources.ids 164 | .idea/**/dataSources.local.xml 165 | .idea/**/sqlDataSources.xml 166 | .idea/**/dynamic.xml 167 | .idea/**/uiDesigner.xml 168 | .idea/**/dbnavigator.xml 169 | 170 | # Gradle 171 | .idea/**/gradle.xml 172 | .idea/**/libraries 173 | 174 | # Gradle and Maven with auto-import 175 | # When using Gradle or Maven with auto-import, you should exclude module files, 176 | # since they will be recreated, and may cause churn. Uncomment if using 177 | # auto-import. 178 | .idea/*.xml 179 | .idea/*.iml 180 | .idea 181 | # .idea/modules 182 | # *.iml 183 | # *.ipr 184 | 185 | # CMake 186 | cmake-build-*/ 187 | 188 | # Mongo Explorer plugin 189 | .idea/**/mongoSettings.xml 190 | 191 | # File-based project format 192 | *.iws 193 | 194 | # IntelliJ 195 | out/ 196 | 197 | # mpeltonen/sbt-idea plugin 198 | .idea_modules/ 199 | 200 | # JIRA plugin 201 | atlassian-ide-plugin.xml 202 | 203 | # Cursive Clojure plugin 204 | .idea/replstate.xml 205 | 206 | # Crashlytics plugin (for Android Studio and IntelliJ) 207 | com_crashlytics_export_strings.xml 208 | crashlytics.properties 209 | crashlytics-build.properties 210 | fabric.properties 211 | 212 | # Editor-based Rest Client 213 | .idea/httpRequests 214 | 215 | # Android studio 3.1+ serialized cache file 216 | .idea/caches/build_file_checksums.ser 217 | 218 | ### PyCharm Patch ### 219 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 220 | 221 | # *.iml 222 | # modules.xml 223 | # .idea/misc.xml 224 | # *.ipr 225 | 226 | # Sonarlint plugin 227 | .idea/sonarlint 228 | 229 | ### Python ### 230 | # Byte-compiled / optimized / DLL files 231 | __pycache__/ 232 | *.py[cod] 233 | *$py.class 234 | 235 | # C extensions 236 | *.so 237 | 238 | # Distribution / packaging 239 | .Python 240 | build/ 241 | develop-eggs/ 242 | dist/ 243 | downloads/ 244 | eggs/ 245 | .eggs/ 246 | lib64/ 247 | parts/ 248 | sdist/ 249 | var/ 250 | wheels/ 251 | pip-wheel-metadata/ 252 | share/python-wheels/ 253 | *.egg-info/ 254 | .installed.cfg 255 | *.egg 256 | MANIFEST 257 | 258 | # PyInstaller 259 | # Usually these files are written by a python script from a template 260 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 261 | *.manifest 262 | *.spec 263 | 264 | # Installer logs 265 | pip-log.txt 266 | pip-delete-this-directory.txt 267 | 268 | # Unit test / coverage reports 269 | htmlcov/ 270 | .tox/ 271 | .nox/ 272 | .coverage 273 | .coverage.* 274 | nosetests.xml 275 | coverage.xml 276 | *.cover 277 | .hypothesis/ 278 | .pytest_cache/ 279 | 280 | # Translations 281 | *.mo 282 | *.pot 283 | 284 | # Django stuff: 285 | local_settings.py 286 | db.sqlite3 287 | db.sqlite3-journal 288 | 289 | # Flask stuff: 290 | instance/ 291 | .webassets-cache 292 | 293 | # Scrapy stuff: 294 | .scrapy 295 | 296 | # Sphinx documentation 297 | docs/_build/ 298 | 299 | # PyBuilder 300 | target/ 301 | 302 | # Jupyter Notebook 303 | .ipynb_checkpoints 304 | 305 | # IPython 306 | profile_default/ 307 | ipython_config.py 308 | 309 | # pyenv 310 | .python-version 311 | 312 | # pipenv 313 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 314 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 315 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 316 | # install all needed dependencies. 317 | #Pipfile.lock 318 | 319 | # celery beat schedule file 320 | celerybeat-schedule 321 | 322 | # SageMath parsed files 323 | *.sage.py 324 | 325 | # Environments 326 | .venv 327 | env/ 328 | venv/ 329 | ENV/ 330 | env.bak/ 331 | venv.bak/ 332 | 333 | # Spyder project settings 334 | .spyderproject 335 | .spyproject 336 | 337 | # Rope project settings 338 | .ropeproject 339 | 340 | # mkdocs documentation 341 | /site 342 | 343 | # mypy 344 | .mypy_cache/ 345 | .dmypy.json 346 | dmypy.json 347 | 348 | # Pyre type checker 349 | .pyre/ 350 | 351 | ### VisualStudioCode ### 352 | .vscode 353 | 354 | ### VisualStudioCode Patch ### 355 | # Ignore all local history of files 356 | .history 357 | 358 | ### Windows ### 359 | # Windows thumbnail cache files 360 | Thumbs.db 361 | Thumbs.db:encryptable 362 | ehthumbs.db 363 | ehthumbs_vista.db 364 | 365 | # Dump file 366 | *.stackdump 367 | 368 | # Folder config file 369 | [Dd]esktop.ini 370 | 371 | # Recycle Bin used on file shares 372 | $RECYCLE.BIN/ 373 | 374 | # Windows Installer files 375 | *.cab 376 | *.msi 377 | *.msix 378 | *.msm 379 | *.msp 380 | 381 | # Windows shortcuts 382 | *.lnk 383 | 384 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode,node 385 | 386 | ### CDK-specific ignores ### 387 | *.swp 388 | cdk.context.json 389 | package-lock.json 390 | yarn.lock 391 | .cdk.staging 392 | cdk.out -------------------------------------------------------------------------------- /functions/updateEstimate.js: -------------------------------------------------------------------------------- 1 | 2 | // original 3 | "use strict"; 4 | 5 | var AWS = require("aws-sdk"); 6 | var dynamoDb = new AWS.DynamoDB.DocumentClient(); 7 | var TABLE_NAME = process.env.TABLE_NAME; 8 | var ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN; // Read allowed origin from environment variable 9 | 10 | 11 | exports.handler = async (event) => { 12 | console.log("[updateEstimate] Received event:", JSON.stringify(event, null, 2)); 13 | 14 | try { 15 | const requestBody = JSON.parse(event.body); 16 | console.log('[updateEstimate] requestBody ', requestBody); 17 | 18 | // Validate the estimateId and servicedetails 19 | if (!requestBody.estimateId || typeof requestBody.estimateId !== "string") { 20 | return { 21 | statusCode: 400, 22 | headers: { 23 | "Content-Type": "application/json", 24 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 25 | "Access-Control-Allow-Methods": "PUT,OPTIONS" 26 | }, 27 | body: JSON.stringify({ message: 'Invalid request: "estimateId" is required and must be a string.' }), 28 | }; 29 | } 30 | 31 | if (!requestBody.servicedetails || typeof requestBody.servicedetails !== "object") { 32 | return { 33 | statusCode: 400, 34 | headers: { 35 | "Content-Type": "application/json", 36 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 37 | "Access-Control-Allow-Methods": "PUT,OPTIONS" 38 | }, 39 | body: JSON.stringify({ message: 'Invalid request: "servicedetails" is required and must be an object.' }), 40 | }; 41 | } 42 | 43 | // Define the params for the DynamoDB update operation 44 | const params = { 45 | TableName: TABLE_NAME, 46 | Key: { estimateId: requestBody.estimateId }, 47 | UpdateExpression: "set servicedetails = :servicedetails", 48 | ExpressionAttributeValues: { 49 | ":servicedetails": requestBody.servicedetails 50 | }, 51 | ReturnValues: "UPDATED_NEW" 52 | }; 53 | 54 | // Perform the update operation in DynamoDB 55 | const data = await dynamoDb.update(params).promise(); 56 | 57 | console.log('[updateEstimate] params ', params); 58 | console.log('[updateEstimate] Updated item data ', data); 59 | 60 | // Return the updated estimate, including the estimateId 61 | return { 62 | statusCode: 200, 63 | headers: { 64 | "Content-Type": "application/json", 65 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 66 | "Access-Control-Allow-Methods": "PUT,OPTIONS" 67 | }, 68 | body: JSON.stringify({ 69 | message: "Estimate updated successfully!", 70 | item: { 71 | estimateId: requestBody.estimateId, // Always include estimateId 72 | ...data.Attributes // Includes only updated fields (servicedeets) 73 | } 74 | }), 75 | }; 76 | 77 | } catch (error) { 78 | console.error("Error updating estimate:", error); 79 | return { 80 | statusCode: 500, 81 | headers: { 82 | "Content-Type": "application/json", 83 | "Access-Control-Allow-Origin": ALLOWED_ORIGIN, 84 | "Access-Control-Allow-Methods": "PUT,OPTIONS" 85 | }, 86 | body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 87 | }; 88 | } 89 | }; 90 | 91 | 92 | // try to accomodate booking-completed 93 | 94 | // "use strict"; 95 | // 96 | // var AWS = require("aws-sdk"); 97 | // var dynamoDb = new AWS.DynamoDB.DocumentClient(); 98 | // var TABLE_NAME = process.env.TABLE_NAME; 99 | // 100 | // exports.handler = async (event) => { 101 | // console.log("[editEstimateById] Received event:", JSON.stringify(event, null, 2)); 102 | // 103 | // try { 104 | // const requestBody = JSON.parse(event.body); 105 | // console.log('[editEstimateById] requestBody:', requestBody); 106 | // console.log('[editEstimateById] requestBody.triggerEvent:', requestBody.triggerEvent); 107 | // //console.log('[editEstimateById] requestBody.type:', requestBody.type); 108 | // console.log('[editEstimateById] event.pathParameters:', event.pathParameters); 109 | // console.log('[editEstimateById] event.pathParameters.estimateId:', event.pathParameters.estimateId); 110 | // 111 | // if (event.pathParameters && event.pathParameters.estimateId) { 112 | // const estimateId = event.pathParameters.estimateId; 113 | // console.log('[editEstimateById] estimateId:', estimateId); 114 | // 115 | // // Check if it's a webhook from Cal.com 116 | // if (requestBody.triggerEvent === 'BOOKING_COMPLETED') { 117 | // const triggerEvent = requestBody.triggerEvent; 118 | // const bookingId = requestBody.payload?.bookingId; // Safely access bookingId 119 | // const userEmail = requestBody.payload?.attendee?.email; // Safely access user email 120 | // 121 | // // Log bookingId and userEmail for debugging 122 | // console.log('[triggerEvent] triggerEvent:', triggerEvent); 123 | // console.log('[editEstimateById] bookingId:', bookingId); 124 | // console.log('[editEstimateById] userEmail:', userEmail); 125 | // 126 | // // Check if bookingId is valid 127 | // if (!bookingId) { 128 | // console.error('Error: bookingId is missing or empty.'); 129 | // return { 130 | // statusCode: 400, 131 | // headers: { 132 | // "Content-Type": "application/json", 133 | // "Access-Control-Allow-Origin": "*", 134 | // "Access-Control-Allow-Methods": "POST,OPTIONS" 135 | // }, 136 | // body: JSON.stringify({ message: 'bookingId is required.' }), 137 | // }; 138 | // } 139 | // 140 | // // Update the estimate with bookingId 141 | // const params = { 142 | // TableName: TABLE_NAME, 143 | // Key: { estimateId: estimateId }, 144 | // UpdateExpression: "set bookingId = :bookingId", 145 | // ExpressionAttributeValues: { 146 | // ":bookingId": bookingId 147 | // }, 148 | // ReturnValues: "UPDATED_NEW" 149 | // }; 150 | // 151 | // // Perform the update 152 | // await dynamoDb.update(params).promise(); 153 | // console.log('Booking ID added to estimate!'); 154 | // 155 | // return { 156 | // statusCode: 200, 157 | // headers: { 158 | // "Content-Type": "application/json", 159 | // "Access-Control-Allow-Origin": "*", 160 | // "Access-Control-Allow-Methods": "PUT,POST,OPTIONS" 161 | // }, 162 | // body: JSON.stringify({ message: "Booking ID updated successfully!" }), 163 | // }; 164 | // } 165 | // } 166 | // 167 | // return { 168 | // statusCode: 400, 169 | // headers: { 170 | // "Content-Type": "application/json", 171 | // "Access-Control-Allow-Origin": "*", 172 | // "Access-Control-Allow-Methods": "PUT,POST,OPTIONS" 173 | // }, 174 | // body: JSON.stringify({ message: "Invalid event or missing estimateId." }), 175 | // }; 176 | // 177 | // } catch (error) { 178 | // console.error("Error updating estimate:", error); 179 | // return { 180 | // statusCode: 500, 181 | // headers: { 182 | // "Content-Type": "application/json", 183 | // "Access-Control-Allow-Origin": "*", 184 | // "Access-Control-Allow-Methods": "PUT,OPTIONS" 185 | // }, 186 | // body: JSON.stringify({ message: "Internal Server Error", error: error.message }), 187 | // }; 188 | // } 189 | // }; 190 | 191 | // rollback code 192 | // "use strict"; 193 | // 194 | // var AWS = require("aws-sdk"); 195 | // var dynamoDb = new AWS.DynamoDB.DocumentClient(); 196 | // var TABLE_NAME = process.env.TABLE_NAME; 197 | // 198 | // exports.handler = async (event) => { 199 | // console.log("[updateEstimate] Received event:", JSON.stringify(event, null, 2)); 200 | // 201 | // try { 202 | // const requestBody = JSON.parse(event.body); 203 | // console.log('[updateEstimate] requestBody: ', requestBody); 204 | // 205 | // // Validate the estimateId 206 | // if (!requestBody.estimateId || typeof requestBody.estimateId !== "string") { 207 | // return { 208 | // statusCode: 400, 209 | // headers: { 210 | // "Content-Type": "application/json", 211 | // "Access-Control-Allow-Origin": "*", 212 | // }, 213 | // body: JSON.stringify({ message: 'Invalid request: "estimateId" is required and must be a string.' }) 214 | // }; 215 | // } 216 | // 217 | // // Validate servicedetails 218 | // if (!requestBody.servicedetails || typeof requestBody.servicedetails !== "object") { 219 | // return { 220 | // statusCode: 400, 221 | // headers: { 222 | // "Content-Type": "application/json", 223 | // "Access-Control-Allow-Origin": "*", 224 | // }, 225 | // body: JSON.stringify({ message: 'Invalid request: "servicedetails" is required and must be an object.' }) 226 | // }; 227 | // } 228 | // 229 | // const params = { 230 | // TableName: TABLE_NAME, 231 | // Key: { estimateId: requestBody.estimateId }, 232 | // UpdateExpression: "set servicedetails = :servicedetails", 233 | // ExpressionAttributeValues: { 234 | // ":servicedetails": requestBody.servicedetails 235 | // }, 236 | // ReturnValues: "UPDATED_NEW" 237 | // }; 238 | // 239 | // const data = await dynamoDb.update(params).promise(); 240 | // console.log('[updateEstimate] Updated estimate: ', data); 241 | // 242 | // // Return the updated estimate 243 | // return { 244 | // statusCode: 200, 245 | // headers: { 246 | // "Content-Type": "application/json", 247 | // "Access-Control-Allow-Origin": "*", 248 | // }, 249 | // body: JSON.stringify({ message: "Estimate updated successfully!", item: data.Attributes }) 250 | // }; 251 | // 252 | // } catch (error) { 253 | // console.error("Error updating estimate:", error); 254 | // return { 255 | // statusCode: 500, 256 | // headers: { 257 | // "Content-Type": "application/json", 258 | // "Access-Control-Allow-Origin": "*", 259 | // }, 260 | // body: JSON.stringify({ message: "Internal Server Error", error: error.message }) 261 | // }; 262 | // } 263 | // }; 264 | // 265 | // 266 | 267 | -------------------------------------------------------------------------------- /lib/rapidcleaners-api-cdk-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { IResource, LambdaIntegration, MockIntegration, PassthroughBehavior, RestApi } from 'aws-cdk-lib/aws-apigateway'; 3 | import { Construct } from 'constructs'; 4 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 5 | import * as api from 'aws-cdk-lib/aws-apigateway'; 6 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 7 | import { App, Stack, RemovalPolicy } from 'aws-cdk-lib'; 8 | import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; 9 | import * as s3 from 'aws-cdk-lib/aws-s3'; 10 | import * as iam from 'aws-cdk-lib/aws-iam'; 11 | import { join } from 'path'; 12 | 13 | export class RapidcleanersApiCdkStack extends cdk.Stack { 14 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 15 | 16 | 17 | // Define the environment - can be 'dev', 'stage', or 'prod' 18 | let environment = 'dev'; // Adjust this manually as needed 19 | console.log('RapidEnvironment:', environment) 20 | 21 | const stackName = `RapidCleanAPI-${environment}`; 22 | 23 | super(scope, stackName, { ...props, stackName }); 24 | 25 | // Correct structure for allowedOrigins 26 | const allowedOriginsMap: { [key: string]: string[] } = { 27 | dev: ['http://localhost:3000'], 28 | stage: ['https://stage.rapidclean.ninja'], 29 | prod: ['https://rapidclean.ninja'], 30 | }; 31 | 32 | // Get allowed origins based on the environment 33 | const currentAllowedOrigins = allowedOriginsMap[environment] || ['*']; 34 | 35 | // Use RemovalPolicy.RETAIN for production and DESTROY for other environments 36 | const removalPolicy = 37 | environment === 'prod' || environment ==='stage' ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY; 38 | 39 | 40 | // DynamoDB Tables 41 | const estimatesTable = new dynamodb.Table(this, `EstimatesTable-${environment}`, { 42 | partitionKey: { name: 'estimateId', type: dynamodb.AttributeType.STRING }, 43 | tableName: `rc-estimates-${environment}`, 44 | removalPolicy: removalPolicy, 45 | }); 46 | 47 | const usersTable = new dynamodb.Table(this, `UsersTable-${environment}`, { 48 | partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, 49 | tableName: `rc-users-${environment}`, 50 | removalPolicy: removalPolicy, 51 | }); 52 | 53 | const locationsTable = new dynamodb.Table(this, `LocationsTable-${environment}}`, { 54 | partitionKey: { name: 'locationId', type: dynamodb.AttributeType.STRING }, 55 | tableName: `rc-locations-${environment}`, 56 | removalPolicy: removalPolicy, 57 | }); 58 | 59 | // GSI for Locations Table 60 | locationsTable.addGlobalSecondaryIndex({ 61 | indexName: 'userId-index', 62 | partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, 63 | projectionType: dynamodb.ProjectionType.ALL, 64 | }); 65 | 66 | const bookingsTable = new dynamodb.Table(this, `RcBookingsTable-${environment}`, { 67 | partitionKey: { name: 'bookingId', type: dynamodb.AttributeType.STRING }, 68 | tableName: `rc-bookings-${environment}`, 69 | removalPolicy: removalPolicy, 70 | }); 71 | 72 | // S3 Buckets 73 | const rcDataBucket = new s3.Bucket(this, `rc-data-s3-${environment}`, { 74 | removalPolicy: RemovalPolicy.DESTROY, 75 | autoDeleteObjects: true, 76 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 77 | }); 78 | 79 | const rcMediaBucket = new s3.Bucket(this, `rc-media-s3-${environment}`, { 80 | removalPolicy: RemovalPolicy.DESTROY, 81 | autoDeleteObjects: environment !== 'prod', 82 | cors: [ 83 | { 84 | allowedOrigins: currentAllowedOrigins, // Use the current environment's allowed origins 85 | allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.PUT], 86 | allowedHeaders: ['*'], 87 | }, 88 | ], 89 | }); 90 | 91 | // Backup role and permissions 92 | const dynamoBackupRole = new iam.Role(this, 'DynamoBackupRole', { 93 | assumedBy: new iam.ServicePrincipal('dynamodb.amazonaws.com'), 94 | }); 95 | 96 | rcDataBucket.grantReadWrite(dynamoBackupRole); 97 | estimatesTable.grantFullAccess(dynamoBackupRole); 98 | usersTable.grantFullAccess(dynamoBackupRole); 99 | locationsTable.grantFullAccess(dynamoBackupRole); 100 | bookingsTable.grantFullAccess(dynamoBackupRole); 101 | 102 | // Lambda function properties 103 | const nodejsFunctionProps: NodejsFunctionProps = { 104 | bundling: { externalModules: ['aws-sdk'] }, 105 | depsLockFilePath: join(__dirname, '../package-lock.json'), 106 | environment: { 107 | ESTIMATES_TABLE_NAME: estimatesTable.tableName, 108 | USERS_TABLE_NAME: usersTable.tableName, 109 | LOCATIONS_TABLE_NAME: locationsTable.tableName, 110 | BUCKET_NAME: rcMediaBucket.bucketName, 111 | }, 112 | runtime: Runtime.NODEJS_16_X, 113 | }; 114 | 115 | // Lambda for Estimates 116 | const createEstimateLambda = new NodejsFunction(this, `createEstimateLambda-${environment}`, { 117 | entry: join(__dirname, '../functions', 'createEstimate.js'), 118 | runtime: Runtime.NODEJS_16_X, 119 | handler: 'handler', 120 | bundling: { 121 | externalModules: ['aws-sdk'], 122 | }, 123 | environment: { 124 | TABLE_NAME: estimatesTable.tableName, 125 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 126 | NODE_ENV: environment, 127 | }, 128 | }); 129 | 130 | const updateEstimateLambda = new NodejsFunction(this, `updateEstimateLambda-${environment}`, { 131 | entry: join(__dirname, '../functions', 'updateEstimate.js'), 132 | runtime: Runtime.NODEJS_16_X, 133 | handler: 'handler', 134 | bundling: { 135 | externalModules: ['aws-sdk'], 136 | }, 137 | environment: { 138 | TABLE_NAME: estimatesTable.tableName, 139 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 140 | NODE_ENV: environment, 141 | }, 142 | }); 143 | 144 | const getAllEstimatesLambda = new NodejsFunction(this, `getAllEstimatesLambda-${environment}`, { 145 | entry: join(__dirname, '../functions', 'getAllEstimates.js'), 146 | runtime: Runtime.NODEJS_16_X, 147 | handler: 'handler', 148 | bundling: { 149 | externalModules: ['aws-sdk'], 150 | }, 151 | environment: { 152 | TABLE_NAME: estimatesTable.tableName, 153 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 154 | NODE_ENV: environment, // Ensure this is correct 155 | }, 156 | }); 157 | 158 | const getOneEstimateLambda = new NodejsFunction(this, `getOneEstimateLambda-${environment}`, { 159 | entry: join(__dirname, '../functions', 'getOneEstimate.js'), 160 | runtime: Runtime.NODEJS_16_X, 161 | handler: 'handler', 162 | bundling: { 163 | externalModules: ['aws-sdk'], 164 | }, 165 | environment: { 166 | TABLE_NAME: estimatesTable.tableName, 167 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 168 | NODE_ENV: environment, // Ensure this is correct 169 | }, 170 | }); 171 | 172 | 173 | const deleteEstimateLambda = new NodejsFunction(this, `deleteEstimateLambda-${environment}`, { 174 | entry: join(__dirname, '../functions', 'deleteEstimate.js'), 175 | ...nodejsFunctionProps, 176 | }); 177 | 178 | // Lambda Functions for CRUD Operations on Users 179 | const getAllUsersLambda = new NodejsFunction(this, `getAllUsersLambda-${environment}`, { 180 | entry: join(__dirname, '../functions', 'getAllUsers.js'), 181 | ...nodejsFunctionProps, 182 | }); 183 | 184 | const getOneUserLambda = new NodejsFunction(this, `getOneUserLambda-${environment}`, { 185 | entry: join(__dirname, '../functions', 'getOneUser.js'), 186 | runtime: Runtime.NODEJS_16_X, 187 | handler: 'handler', 188 | bundling: { 189 | externalModules: ['aws-sdk'], 190 | }, 191 | environment: { 192 | TABLE_NAME: usersTable.tableName, 193 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 194 | NODE_ENV: environment,// Ensure this is correct 195 | }, 196 | }); 197 | 198 | const createUserLambda = new NodejsFunction(this, `createUserLambda-${environment}`, { 199 | entry: join(__dirname, '../functions', 'createUser.js'), 200 | runtime: Runtime.NODEJS_16_X, 201 | handler: 'handler', 202 | bundling: { 203 | externalModules: ['aws-sdk'], 204 | }, 205 | environment: { 206 | TABLE_NAME: usersTable.tableName, 207 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 208 | NODE_ENV: environment,// Ensure this is correct 209 | }, 210 | }); 211 | 212 | const updateUserLambda = new NodejsFunction(this, `updateUserLambda-${environment}`, { 213 | entry: join(__dirname, '../functions', 'updateUser.js'), 214 | ...nodejsFunctionProps, 215 | }); 216 | 217 | const deleteUserLambda = new NodejsFunction(this, `deleteUserLambda-${environment}`, { 218 | entry: join(__dirname, '../functions', 'deleteUser.js'), 219 | ...nodejsFunctionProps, 220 | }); 221 | 222 | // Inside the constructor 223 | 224 | // Temporary Validation - Lambda function for userId -> EstimateId validation 225 | const validateUserLambda = new NodejsFunction(this, `validateUserLambda-${environment}`, { 226 | entry: join(__dirname, '../functions', 'validateUser.js'), 227 | runtime: Runtime.NODEJS_16_X, 228 | handler: 'handler', 229 | bundling: { 230 | externalModules: ['aws-sdk'], 231 | }, 232 | environment: { 233 | TABLE_NAME: estimatesTable.tableName, 234 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 235 | NODE_ENV: environment, 236 | }, 237 | }); 238 | 239 | 240 | 241 | 242 | 243 | 244 | // Lambda Functions for CRUD Operations on Locations 245 | const getAllLocationsLambda = new NodejsFunction(this, `getAllLocationsLambda-${environment}`, { 246 | entry: join(__dirname, '../functions', 'getAllLocations.js'), 247 | ...nodejsFunctionProps, 248 | }); 249 | 250 | const getOneLocationLambda = new NodejsFunction(this, `getOneLocationLambda-${environment}`, { 251 | entry: join(__dirname, '../functions', 'getOneLocation.js'), 252 | runtime: Runtime.NODEJS_16_X, 253 | handler: 'handler', 254 | bundling: { 255 | externalModules: ['aws-sdk'], 256 | }, 257 | environment: { 258 | TABLE_NAME: locationsTable.tableName, 259 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 260 | NODE_ENV: environment,// Ensure this is correct 261 | }, 262 | }); 263 | 264 | const getOneLocationByUserId = new NodejsFunction(this, `getOneLocationByUserIdLambda-${environment}`, { 265 | entry: join(__dirname, '../functions', 'getOneLocationByUserId.js'), 266 | runtime: Runtime.NODEJS_16_X, 267 | handler: 'handler', 268 | bundling: { 269 | externalModules: ['aws-sdk'], 270 | }, 271 | environment: { 272 | TABLE_NAME: locationsTable.tableName, 273 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 274 | NODE_ENV: environment,// Ensure this is correct 275 | }, 276 | }); 277 | 278 | const createLocationLambda = new NodejsFunction(this, `createLocationLambda-${environment}`, { 279 | entry: join(__dirname, '../functions', 'createLocation.js'), 280 | runtime: Runtime.NODEJS_16_X, 281 | handler: 'handler', 282 | bundling: { 283 | externalModules: ['aws-sdk'], 284 | }, 285 | environment: { 286 | TABLE_NAME: locationsTable.tableName, 287 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 288 | NODE_ENV: environment,// Ensure this is correct 289 | }, 290 | }); 291 | 292 | 293 | const updateLocationLambda = new NodejsFunction(this, `updateLocationLambda-${environment}`, { 294 | entry: join(__dirname, '../functions', 'updateLocation.js'), 295 | ...nodejsFunctionProps, 296 | }); 297 | 298 | const deleteLocationLambda = new NodejsFunction(this, `deleteLocationLambda-${environment}`, { 299 | entry: join(__dirname, '../functions', 'deleteLocation.js'), 300 | ...nodejsFunctionProps, 301 | }); 302 | 303 | const createBookingLambda = new NodejsFunction(this, `createBookingLambda-${environment}`, { 304 | entry: join(__dirname, '../functions', 'createBooking.js'), 305 | runtime: Runtime.NODEJS_16_X, 306 | handler: 'handler', 307 | bundling: { 308 | externalModules: ['aws-sdk'], 309 | }, 310 | environment: { 311 | TABLE_NAME: bookingsTable.tableName, 312 | NODE_ENV: environment,// Ensure this is correct 313 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 314 | }, 315 | }); 316 | 317 | const getAllBookingsLambda = new NodejsFunction(this, `getAllBookingsLambda-${environment}`, { 318 | entry: join(__dirname, '../functions', 'getAllBookings.js'), 319 | runtime: Runtime.NODEJS_16_X, 320 | handler: 'handler', 321 | bundling: { 322 | externalModules: ['aws-sdk'], 323 | }, 324 | environment: { 325 | TABLE_NAME: bookingsTable.tableName, 326 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 327 | NODE_ENV: environment,// Ensure this is correct 328 | }, 329 | }); 330 | 331 | const updateBookingLambda = new NodejsFunction(this, `updateBookingLambda-${environment}`, { 332 | entry: join(__dirname, '../functions', 'updateBooking.js'), 333 | runtime: Runtime.NODEJS_16_X, 334 | handler: 'handler', 335 | bundling: { 336 | externalModules: ['aws-sdk'], 337 | }, 338 | environment: { 339 | TABLE_NAME: bookingsTable.tableName, 340 | ALLOWED_ORIGIN: currentAllowedOrigins[0], 341 | NODE_ENV: environment,// Ensure this is correct 342 | }, 343 | }); 344 | 345 | // Grant DynamoDB permissions to Lambda functions 346 | estimatesTable.grantReadWriteData(getAllEstimatesLambda); 347 | estimatesTable.grantReadWriteData(getOneEstimateLambda); 348 | estimatesTable.grantReadWriteData(createEstimateLambda); 349 | estimatesTable.grantReadWriteData(updateEstimateLambda); 350 | estimatesTable.grantReadWriteData(deleteEstimateLambda); 351 | estimatesTable.grantReadData(validateUserLambda); 352 | 353 | usersTable.grantReadWriteData(getAllUsersLambda); 354 | usersTable.grantReadWriteData(getOneUserLambda); 355 | usersTable.grantReadWriteData(createUserLambda); 356 | usersTable.grantReadWriteData(updateUserLambda); 357 | usersTable.grantReadWriteData(deleteUserLambda); 358 | 359 | locationsTable.grantReadWriteData(getAllLocationsLambda); 360 | locationsTable.grantReadWriteData(getOneLocationLambda); 361 | locationsTable.grantReadWriteData(getOneLocationByUserId); 362 | locationsTable.grantReadWriteData(createLocationLambda); 363 | locationsTable.grantReadWriteData(updateLocationLambda); 364 | locationsTable.grantReadWriteData(deleteLocationLambda); 365 | 366 | bookingsTable.grantReadWriteData(createBookingLambda); 367 | //bookingsTable.grantReadWriteData(updateBookingLambda); 368 | //bookingsTable.grantReadWriteData(getAllBookingsLambda); 369 | 370 | 371 | // API Gateway Setup 372 | const rapidcleanAPI = new api.RestApi(this, `RapidCleanAPI-${environment}`, { 373 | restApiName: `rc-service-${environment}`, 374 | description: 'AWS API Gateway with Lambda Proxy integration', 375 | deployOptions: { stageName: environment }, // Adjust this based on environment 376 | }); 377 | 378 | // added this from experience 379 | addCorsOptions(rapidcleanAPI.root, currentAllowedOrigins); 380 | 381 | //validation root 382 | // API Gateway Resource for Estimate User Validation 383 | const validation = rapidcleanAPI.root.addResource('validate'); 384 | validation.addMethod('POST', new api.LambdaIntegration(validateUserLambda)); 385 | addCorsOptions(validation, currentAllowedOrigins); 386 | 387 | //Bookings root 388 | const bookings = rapidcleanAPI.root.addResource('bookings'); 389 | bookings.addMethod('POST', new api.LambdaIntegration(createBookingLambda)); 390 | addCorsOptions(bookings, currentAllowedOrigins); 391 | 392 | const estimates = rapidcleanAPI.root.addResource('estimates'); 393 | estimates.addMethod('POST', new api.LambdaIntegration(createEstimateLambda)); 394 | //bookings.addMethod('GET', new api.LambdaIntegration(getAllBookingsLambda)); 395 | addCorsOptions(estimates, currentAllowedOrigins); 396 | 397 | 398 | const singleEstimate = estimates.addResource('{estimateId}'); 399 | singleEstimate.addMethod('GET', new api.LambdaIntegration(getOneEstimateLambda)); 400 | singleEstimate.addMethod('PUT', new api.LambdaIntegration(updateEstimateLambda)); 401 | singleEstimate.addMethod('DELETE', new api.LambdaIntegration(deleteEstimateLambda)); 402 | addCorsOptions(singleEstimate, currentAllowedOrigins); 403 | 404 | // API Gateway Resources and Methods for Users 405 | const users = rapidcleanAPI.root.addResource('users'); 406 | users.addMethod('GET', new api.LambdaIntegration(getAllUsersLambda)); 407 | users.addMethod('POST', new api.LambdaIntegration(createUserLambda)); 408 | addCorsOptions(users, currentAllowedOrigins); 409 | 410 | const singleUser = users.addResource('{userId}'); 411 | singleUser.addMethod('GET', new api.LambdaIntegration(getOneUserLambda)); 412 | singleUser.addMethod('PUT', new api.LambdaIntegration(updateUserLambda)); 413 | singleUser.addMethod('DELETE', new api.LambdaIntegration(deleteUserLambda)); 414 | addCorsOptions(singleUser, currentAllowedOrigins); 415 | 416 | 417 | // API Gateway Resources and Methods for Locations 418 | const locations = rapidcleanAPI.root.addResource('locations'); 419 | locations.addMethod('GET', new api.LambdaIntegration(getAllLocationsLambda)); 420 | locations.addMethod('POST', new api.LambdaIntegration(createLocationLambda)); 421 | addCorsOptions(locations, currentAllowedOrigins); 422 | 423 | const singleLocation = locations.addResource('{locationId}'); 424 | singleLocation.addMethod('GET', new api.LambdaIntegration(getOneLocationLambda)); 425 | singleLocation.addMethod('PUT', new api.LambdaIntegration(updateLocationLambda)); 426 | singleLocation.addMethod('DELETE', new api.LambdaIntegration(deleteLocationLambda)); 427 | addCorsOptions(singleLocation, currentAllowedOrigins); 428 | 429 | const locationByUser = locations.addResource('user').addResource('{userId}'); 430 | locationByUser.addMethod('GET', new api.LambdaIntegration(getOneLocationByUserId)); // Get one location by userId 431 | addCorsOptions(locationByUser, currentAllowedOrigins); 432 | 433 | 434 | 435 | // Output API Gateway URL 436 | new cdk.CfnOutput(this, 'HTTP API Url', { 437 | value: rapidcleanAPI.url ?? 'Something went wrong with the deploy', 438 | }); 439 | } 440 | } 441 | 442 | // CORS configuration function 443 | export function addCorsOptions(apiResource: IResource, allowedOrigins: string[]) { 444 | const origin = allowedOrigins.join(', '); // Join allowed origins into a string 445 | 446 | apiResource.addMethod('OPTIONS', new MockIntegration({ 447 | integrationResponses: [{ 448 | statusCode: '200', 449 | responseParameters: { 450 | 'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", 451 | 'method.response.header.Access-Control-Allow-Methods': "'GET,POST,PUT,OPTIONS'", 452 | 'method.response.header.Access-Control-Allow-Origin': `'${origin}'`, // Properly wrap in quotes 453 | 'method.response.header.Access-Control-Max-Age': "'600'", // Disable CORS caching for testing 454 | }, 455 | }], 456 | passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH, 457 | requestTemplates: { 458 | "application/json": "{\"statusCode\": 200}" 459 | }, 460 | }), { 461 | methodResponses: [{ 462 | statusCode: '200', 463 | responseParameters: { 464 | 'method.response.header.Access-Control-Allow-Headers': true, 465 | 'method.response.header.Access-Control-Allow-Methods': true, 466 | 'method.response.header.Access-Control-Allow-Origin': true, 467 | 'method.response.header.Access-Control-Max-Age': true, 468 | }, 469 | }], 470 | }); 471 | } 472 | --------------------------------------------------------------------------------