diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..99f2ed43 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,90 @@ +# API Gateway Resource Policy Update Script + +This script updates your API Gateway resource policy after your CDK deployment to allow only Cognito authenticated users and deny non-HTTPS requests. Using a separate script avoids the circular dependency issues that can occur when adding policies in CDK. + +## Setup + +1. Save the script to a file named `update-api-policy.js` in your project. + +2. Install the AWS SDK if you haven't already: + ```bash + npm install aws-sdk + ``` + +3. Make the script executable: + ```bash + chmod +x update-api-policy.js + ``` + +## Configuration + +Update the following variables in the script to match your environment: + +- `STACK_NAME`: The name of your CloudFormation stack (e.g., 'ai-team-medical-reports-stack-development') +- `REGION`: Your AWS region (e.g., 'us-east-1') +- `API_NAME`: The name of your API Gateway (e.g., 'AIMedicalReport-development') + +## Usage + +You can run the script after each successful CDK deployment: + +```bash +# Run CDK deployment first +cdk deploy ai-team-medical-reports-stack-development + +# Then run the policy update script +./update-api-policy.js +``` + +Alternatively, you can set the Cognito User Pool ID as an environment variable: + +```bash +COGNITO_USER_POOL_ID=us-east-1_yourPoolId ./update-api-policy.js +``` + +## Automation + +To automatically run this after each deployment, you can create a simple shell script: + +```bash +#!/bin/bash +# deploy-and-update.sh + +# Deploy with CDK +cdk deploy ai-team-medical-reports-stack-development + +# If deployment was successful, update the API policy +if [ $? -eq 0 ]; then + echo "CDK deployment successful, updating API policy..." + ./update-api-policy.js +else + echo "CDK deployment failed, skipping API policy update." + exit 1 +fi +``` + +Make it executable: +```bash +chmod +x deploy-and-update.sh +``` + +## Troubleshooting + +If you encounter any issues: + +1. **Authentication errors**: Make sure your AWS credentials are configured correctly with the necessary permissions. + +2. **API not found**: Verify the API_NAME matches exactly what's in your AWS Console. + +3. **Stack not found**: Check that the STACK_NAME is correct. + +4. **Cognito User Pool ID not found**: You can set it manually with the COGNITO_USER_POOL_ID environment variable. + +## Security Considerations + +This script sets a resource policy that: + +1. Allows only authenticated Cognito users to access your API +2. Denies any non-HTTPS requests to your API + +If you need more complex permissions, you can modify the policy object in the script. diff --git a/backend/package-lock.json b/backend/package-lock.json index 431b674c..f81914ec 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,6 +20,8 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.13", "@types/jest": "^29.5.12", + "aws-cdk-lib": "^2.185.0", + "aws-sdk": "^2.1692.0", "axios": "^1.8.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -54,7 +56,7 @@ "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-c8": "^0.33.0", "aws-cdk": "2.139.0", - "aws-cdk-lib": "^2.184.1", + "aws-cdk-lib": "^2.185.0", "dotenv-cli": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -72,7 +74,7 @@ "vitest": "^0.33.0" }, "peerDependencies": { - "aws-cdk-lib": "2.184.1" + "aws-cdk-lib": "^2.185.0" } }, "node_modules/@ampproject/remapping": { @@ -4660,6 +4662,21 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/aws-cdk": { "version": "2.139.0", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.139.0.tgz", @@ -4677,9 +4694,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.184.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.184.1.tgz", - "integrity": "sha512-No9g0SGadiDz0IEUIeJg4wSV/jFCGcouW2zUOTjV8OU4gTMoGiqC8BYSv7E6ucUtW6rmSFVK+pbc8XOFZOo1cg==", + "version": "2.185.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.185.0.tgz", + "integrity": "sha512-RNcQeNnInumDF1hq3gAf+/A6jhvYDof5a7418gEs/y6359gTYZpTCQkgItC50iV3MmkgerrBAdOE7CDEtQNDWw==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -5076,6 +5093,63 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", @@ -5224,7 +5298,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5525,7 +5598,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -6213,7 +6285,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7304,6 +7375,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -7724,7 +7810,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7979,6 +8064,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8006,6 +8107,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8052,6 +8165,24 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8095,6 +8226,24 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8108,6 +8257,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9044,6 +9208,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10291,6 +10464,15 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -10468,6 +10650,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10845,12 +11036,35 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -10993,7 +11207,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -12135,6 +12348,35 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12568,6 +12810,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -12650,6 +12913,28 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 7d20ce1f..61bf11af 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,15 +28,18 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/util-dynamodb": "^3.758.0", "@aws-sdk/client-secrets-manager": "^3.758.0", + "@aws-sdk/util-dynamodb": "^3.758.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", - "@nestjs/platform-express": "^10.0.0", "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.1.13", "@types/jest": "^29.5.12", + "aws-cdk-lib": "^2.185.0", + "aws-sdk": "^2.1692.0", "axios": "^1.8.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -47,15 +50,13 @@ "helmet": "^7.0.0", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.5", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "source-map-support": "^0.5.21", - "web-vitals": "^2.1.4", - "aws-cdk-lib": "2.184.1", - "@nestjs/swagger": "^7.1.13", "swagger-ui-express": "^5.0.0", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1" + "web-vitals": "^2.1.4" }, "devDependencies": { "@aws-cdk/assert": "^2.68.0", @@ -73,7 +74,7 @@ "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-c8": "^0.33.0", "aws-cdk": "2.139.0", - "aws-cdk-lib": "^2.184.1", + "aws-cdk-lib": "^2.185.0", "dotenv-cli": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -91,6 +92,6 @@ "vitest": "^0.33.0" }, "peerDependencies": { - "aws-cdk-lib": "2.184.1" + "aws-cdk-lib": "^2.185.0" } } diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index ae522ffe..b5fb3645 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -2,8 +2,12 @@ import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as logs from 'aws-cdk-lib/aws-logs'; -import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; + import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { RemovalPolicy } from 'aws-cdk-lib'; @@ -12,8 +16,6 @@ interface BackendStackProps extends cdk.StackProps { environment: string; cognitoClientId: string; cognitoUserPoolId: string; - domainName?: string; // Optional domain name for certificate - hostedZoneId?: string; // Optional hosted zone ID for domain } export class BackendStack extends cdk.Stack { @@ -23,26 +25,34 @@ export class BackendStack extends cdk.Stack { const isProd = props.environment === 'production'; const appName = 'AIMedicalReport'; - // Look up existing VPC or create a new one - const vpc: ec2.IVpc = new ec2.Vpc(this, `${appName}VPC`, { + // VPC + const vpc = new ec2.Vpc(this, `${appName}VPC`, { vpcName: `${appName}VPC-${props.environment}`, maxAzs: 2, + natGateways: isProd ? 2 : 1, }); + // ECS Cluster const cluster = new ecs.Cluster(this, `${appName}Cluster`, { vpc, clusterName: `${appName}Cluster-${props.environment}`, containerInsights: true, + enableFargateCapacityProviders: true, }); - // Create Log Group for container + // CloudMap Namespace for service discovery + cluster.addDefaultCloudMapNamespace({ + name: `${appName.toLowerCase()}.local`, + }); + + // Log Group const logGroup = new logs.LogGroup(this, `${appName}LogGroup`, { logGroupName: `/ecs/${appName}-${props.environment}`, retention: isProd ? logs.RetentionDays.ONE_MONTH : logs.RetentionDays.ONE_WEEK, removalPolicy: isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, }); - // Create DynamoDB table for reports + // DynamoDB table for reports const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { tableName: `${appName}ReportsTable${props.environment}`, partitionKey: { @@ -57,7 +67,7 @@ export class BackendStack extends cdk.Stack { removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY, }); - // Add a GSI for querying by date (most recent first) + // Add GSI for querying by date reportsTable.addGlobalSecondaryIndex({ indexName: 'userIdDateIndex', partitionKey: { @@ -70,20 +80,50 @@ export class BackendStack extends cdk.Stack { }, }); - // Look up existing Cognito User Pool - const userPoolId = - props.cognitoUserPoolId || - cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; + // Cognito User Pool + const userPool = cognito.UserPool.fromUserPoolId( + this, + `${appName}UserPool`, + props.cognitoUserPoolId || 'us-east-1_PszlvSmWc', + ); - // Create a Cognito domain if it doesn't exist + // Cognito domain const userPoolDomain = cognito.UserPoolDomain.fromDomainName( this, `${appName}ExistingDomain-${props.environment}`, - 'us-east-1pszlvsmwc', // The domain prefix without the .auth.region.amazoncognito.com part + 'us-east-1pszlvsmwc', + ); + + // User Pool Client + const userPoolClient = cognito.UserPoolClient.fromUserPoolClientId( + this, + `${appName}UserPoolClient-${props.environment}`, + props.cognitoClientId, + ); + + // Security Group for Fargate service + const serviceSecurityGroup = new ec2.SecurityGroup( + this, + `${appName}ServiceSG-${props.environment}`, + { + vpc, + allowAllOutbound: true, + description: 'Security group for Fargate service', + }, + ); + + // Add inbound rules to allow traffic from API Gateway + serviceSecurityGroup.addIngressRule( + ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Port.tcp(3000), + 'Allow inbound HTTP traffic from within VPC', ); - // Replace the userPoolClient reference with a direct reference to the client ID - const userPoolClientId = props.cognitoClientId; + serviceSecurityGroup.addIngressRule( + ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Port.tcp(3443), + 'Allow inbound HTTPS traffic from within VPC', + ); // Task Definition const taskDefinition = new ecs.FargateTaskDefinition( @@ -95,6 +135,29 @@ export class BackendStack extends cdk.Stack { }, ); + // Grant DynamoDB permissions to task + reportsTable.grantReadWriteData(taskDefinition.taskRole); + + // Create a secrets manager for the SSL certificate and key + const certificateSecret = new cdk.aws_secretsmanager.Secret( + this, + `${appName}CertSecret-${props.environment}`, + { + secretName: `${appName}/ssl-cert-${props.environment}`, + description: 'SSL certificate and private key for HTTPS', + generateSecretString: { + secretStringTemplate: JSON.stringify({ + // You'll need to populate these values after deployment + certificate: + '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', + privateKey: + '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----', + }), + generateStringKey: 'dummy', // This key won't be used but is required + }, + }, + ); + // Container const container = taskDefinition.addContainer(`${appName}Container-${props.environment}`, { image: ecs.ContainerImage.fromAsset('../backend/', { @@ -107,17 +170,22 @@ export class BackendStack extends cdk.Stack { // Basic environment variables NODE_ENV: props.environment, PORT: '3000', + HTTPS_PORT: '3443', // Add HTTPS port + ENABLE_HTTPS: 'true', // Enable HTTPS // AWS related AWS_REGION: this.region, - AWS_COGNITO_USER_POOL_ID: userPoolId, - AWS_COGNITO_CLIENT_ID: userPoolClientId, + AWS_COGNITO_USER_POOL_ID: userPool.userPoolId, + AWS_COGNITO_CLIENT_ID: userPoolClient.userPoolClientId, DYNAMODB_REPORTS_TABLE: reportsTable.tableName, // Perplexity related PERPLEXITY_API_KEY_SECRET_NAME: `medical-reports-explainer/${props.environment}/perplexity-api-key`, PERPLEXITY_MODEL: 'sonar', PERPLEXITY_MAX_TOKENS: '2048', + + // SSL Certificate secret + SSL_CERT_SECRET_NAME: certificateSecret.secretName, }, logging: ecs.LogDrivers.awsLogs({ streamPrefix: appName, @@ -125,67 +193,38 @@ export class BackendStack extends cdk.Stack { }), }); + // Grant the task role access to read the SSL certificate secret + certificateSecret.grantRead(taskDefinition.taskRole); + container.addPortMappings({ containerPort: 3000, + name: 'http-api', protocol: ecs.Protocol.TCP, }); - // 1. Create ALB - const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { - vpc, - internetFacing: true, - loadBalancerName: `${appName}-${props.environment}`, - }); - - // 2. Create ALB Target Group - const targetGroup = new elbv2.ApplicationTargetGroup( - this, - `${appName}TargetGroup-${props.environment}`, - { - vpc, - port: 3000, - protocol: elbv2.ApplicationProtocol.HTTP, - targetType: elbv2.TargetType.IP, - healthCheck: { - path: '/api/health', - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), - }, - }, - ); - - // 3. HTTP 80 Listener - const httpListener = alb.addListener(`${appName}HttpListener-${props.environment}`, { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - defaultAction: elbv2.ListenerAction.forward([targetGroup]), + container.addPortMappings({ + containerPort: 3443, + name: 'https-api', + protocol: ecs.Protocol.TCP, }); - // 4. Create a security group for the Fargate service - const serviceSecurityGroup = new ec2.SecurityGroup( - this, - `${appName}ServiceSG-${props.environment}`, - { - vpc, - allowAllOutbound: true, - }, - ); - - // 5. Create the Fargate service WITHOUT registering it with the target group yet + // Create Fargate Service with CloudMap service discovery const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { cluster, taskDefinition, desiredCount: isProd ? 2 : 1, - assignPublicIp: false, securityGroups: [serviceSecurityGroup], + assignPublicIp: false, // Using private subnets with NAT gateway + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + cloudMapOptions: { + name: `${appName.toLowerCase()}-service`, + dnsRecordType: servicediscovery.DnsRecordType.A, + dnsTtl: cdk.Duration.seconds(30), + container: container, + containerPort: 3000, + }, }); - // 6. Add explicit dependency to ensure the listener exists before the service - fargateService.node.addDependency(httpListener); - - // 7. Now register the service with the target group - targetGroup.addTarget(fargateService); - // Add autoscaling for production if (isProd) { const scaling = fargateService.autoScaleTaskCount({ @@ -200,22 +239,229 @@ export class BackendStack extends cdk.Stack { }); } - // Add output for the table name + // Create a Network Load Balancer for the Fargate service + const nlb = new elbv2.NetworkLoadBalancer(this, `${appName}NLB-${props.environment}`, { + vpc, + internetFacing: false, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // Add a listener to the NLB + // Security note: This NLB is internal-only within a private subnet and not internet-facing. + // External traffic is secured via API Gateway's HTTPS endpoints, so unencrypted internal + // communication within the VPC's private network boundary is acceptable here. + const listener = nlb.addListener(`${appName}Listener-${props.environment}`, { + port: 80, + protocol: elbv2.Protocol.TCP, + }); + + // Add the Fargate service as a target to the listener + listener.addTargets(`${appName}TargetGroup-${props.environment}`, { + targets: [fargateService], + port: 3000, + protocol: elbv2.Protocol.TCP, + healthCheck: { + enabled: true, + protocol: elbv2.Protocol.HTTP, + path: '/api/health', + interval: cdk.Duration.seconds(30), + healthyThresholdCount: 2, + unhealthyThresholdCount: 2, + timeout: cdk.Duration.seconds(5), + }, + }); + + // Create VPC Link for API Gateway using the NLB + const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + targets: [nlb], + description: `VPC Link for ${appName} ${props.environment}`, + }); + + // Create API Gateway first without any resources or methods + const apiLogicalId = `${appName}-api-${props.environment}`; + const api = new apigateway.RestApi(this, apiLogicalId, { + restApiName: `${appName}-${props.environment}`, + description: `API for ${appName} ${props.environment}`, + deployOptions: { + stageName: props.environment, + loggingLevel: apigateway.MethodLoggingLevel.INFO, + dataTraceEnabled: true, + }, + // Important: Do NOT use defaultCorsPreflightOptions here + }); + + // Create Cognito Authorizer + const authorizer = new apigateway.CognitoUserPoolsAuthorizer( + this, + `${appName}Authorizer-${props.environment}`, + { + cognitoUserPools: [userPool], + authorizerName: `${appName}Authorizer-${props.environment}`, + identitySource: 'method.request.header.Authorization', + }, + ); + + // Define the service URL using the NLB DNS + const serviceUrl = `http://${nlb.loadBalancerDnsName}`; + + // Create the 'api' resource + const apiResource = api.root.addResource('api'); + + // Create the 'reports' resource under 'api' + const reportsResource = apiResource.addResource('reports'); + + // Create the 'latest' resource under 'reports' + const latestResource = reportsResource.addResource('latest'); + + // Create the ':id' resource under 'reports' with a path parameter + const reportIdResource = reportsResource.addResource('{id}'); + + // Create the 'status' resource under ':id' + const reportStatusResource = reportIdResource.addResource('status'); + + // Define integration options once for reuse + const integrationOptions = { + connectionType: apigateway.ConnectionType.VPC_LINK, + vpcLink: vpcLink, + }; + + // Create integrations for each endpoint + const getReportsIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/reports`, + options: integrationOptions, + }); + + const getLatestReportIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/reports/latest`, + options: integrationOptions, + }); + + const getReportByIdIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/reports/{id}`, + options: { + ...integrationOptions, + requestParameters: { + 'integration.request.path.id': 'method.request.path.id', + }, + }, + }); + + const patchReportStatusIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'PATCH', + uri: `${serviceUrl}/api/reports/{id}/status`, + options: { + ...integrationOptions, + requestParameters: { + 'integration.request.path.id': 'method.request.path.id', + }, + }, + }); + + // Define method options with authorization + const methodOptions = { + authorizer: authorizer, + authorizationType: apigateway.AuthorizationType.COGNITO, + }; + + // Add methods to the resources + reportsResource.addMethod('GET', getReportsIntegration, methodOptions); + latestResource.addMethod('GET', getLatestReportIntegration, methodOptions); + + // For path parameter methods, add the request parameter configuration + reportIdResource.addMethod('GET', getReportByIdIntegration, { + ...methodOptions, + requestParameters: { + 'method.request.path.id': true, + }, + }); + + reportStatusResource.addMethod('PATCH', patchReportStatusIntegration, { + ...methodOptions, + requestParameters: { + 'method.request.path.id': true, + }, + }); + + // Add CORS to each resource separately - after methods have been created + const corsOptions = { + allowOrigins: ['*'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'], + maxAge: cdk.Duration.seconds(300), + }; + + // Add CORS to all resources + api.root.addCorsPreflight(corsOptions); + apiResource.addCorsPreflight(corsOptions); + reportsResource.addCorsPreflight(corsOptions); + latestResource.addCorsPreflight(corsOptions); + reportIdResource.addCorsPreflight(corsOptions); + reportStatusResource.addCorsPreflight(corsOptions); + + // Apply resource policy separately after resources and methods are created + // const apiResourcePolicy = new iam.PolicyDocument({ + // statements: [ + // // Allow authenticated Cognito users + // new iam.PolicyStatement({ + // effect: iam.Effect.ALLOW, + // principals: [new iam.AnyPrincipal()], + // actions: ['execute-api:Invoke'], + // resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], + // }), + // // Deny non-HTTPS requests + // new iam.PolicyStatement({ + // effect: iam.Effect.DENY, + // principals: [new iam.AnyPrincipal()], + // actions: ['execute-api:Invoke'], + // resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], + // conditions: { + // Bool: { + // 'aws:SecureTransport': 'false', + // }, + // }, + // }), + // ], + // }); + + // Create API Gateway execution role with required permissions + new iam.Role(this, `${appName}APIGatewayRole-${props.environment}`, { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'AmazonVPCCrossAccountNetworkInterfaceOperations', + ), + ], + }); + + // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, description: 'DynamoDB Reports Table Name', }); - // Add output for Cognito domain new cdk.CfnOutput(this, 'CognitoDomain', { value: `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com`, description: 'Cognito Domain URL', }); - // Outputs - new cdk.CfnOutput(this, 'LoadBalancerDNS', { - value: alb.loadBalancerDnsName, - description: 'Load Balancer DNS Name', + new cdk.CfnOutput(this, 'ApiGatewayUrl', { + value: api.url, + description: 'API Gateway URL', + }); + + new cdk.CfnOutput(this, 'NetworkLoadBalancerDns', { + value: nlb.loadBalancerDnsName, + description: 'Network Load Balancer DNS Name', }); } } diff --git a/backend/src/iac/deploy.sh b/backend/src/iac/deploy.sh new file mode 100755 index 00000000..458f6401 --- /dev/null +++ b/backend/src/iac/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Run CDK deployment +cdk deploy ai-team-medical-reports-stack-development + +# If successful, update the API policy +if [ $? -eq 0 ]; then + ./update-api-policy.js +fi diff --git a/backend/src/iac/update-api-policy.js b/backend/src/iac/update-api-policy.js new file mode 100755 index 00000000..35f59502 --- /dev/null +++ b/backend/src/iac/update-api-policy.js @@ -0,0 +1,115 @@ +#!/usr/bin/env node +const { APIGateway, CloudFormation } = require('aws-sdk'); +const fs = require('fs'); +const path = require('path'); + +// Configuration - update these values +const STACK_NAME = 'ai-team-medical-reports-stack-development'; +const REGION = 'us-east-1'; // Update with your region +const API_NAME = 'AIMedicalReport-development'; // Your API Gateway name + +async function main() { + try { + console.log('Starting API Gateway policy update script...'); + + // Initialize AWS clients + const cloudformation = new CloudFormation({ region: REGION }); + const apigateway = new APIGateway({ region: REGION }); + + // Get the stack outputs to find resources + console.log(`Getting information from CloudFormation stack: ${STACK_NAME}`); + const stackResponse = await cloudformation.describeStacks({ + StackName: STACK_NAME + }).promise(); + + if (!stackResponse.Stacks || stackResponse.Stacks.length === 0) { + throw new Error(`Stack ${STACK_NAME} not found`); + } + + // Find the API Gateway and Cognito User Pool ID from stack outputs + const outputs = stackResponse.Stacks[0].Outputs || []; + let cognitoUserPoolId = null; + + for (const output of outputs) { + if (output.OutputKey === 'UserPoolId') { + cognitoUserPoolId = output.OutputValue; + console.log(`Found Cognito User Pool ID: ${cognitoUserPoolId}`); + } + } + + // If Cognito ID wasn't found in outputs, prompt for it + if (!cognitoUserPoolId) { + cognitoUserPoolId = process.env.COGNITO_USER_POOL_ID; + if (!cognitoUserPoolId) { + console.log('Cognito User Pool ID not found in stack outputs.'); + console.log('Please set the COGNITO_USER_POOL_ID environment variable and try again.'); + process.exit(1); + } + } + + // Find the API Gateway + console.log('Listing API Gateways...'); + const apis = await apigateway.getRestApis().promise(); + + const api = apis.items.find(api => api.name === API_NAME); + if (!api) { + throw new Error(`API Gateway with name ${API_NAME} not found`); + } + + console.log(`Found API Gateway: ${api.name} (${api.id})`); + + // Create the resource policy + const policy = { + Version: '2012-10-17', + Statement: [ + // Allow authenticated Cognito users + { + Effect: 'Allow', + Principal: '*', + Action: 'execute-api:Invoke', + Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`, + Condition: { + StringEquals: { + 'cognito-identity.amazonaws.com:aud': cognitoUserPoolId + } + } + }, + // Deny non-HTTPS requests + { + Effect: 'Deny', + Principal: '*', + Action: 'execute-api:Invoke', + Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`, + Condition: { + Bool: { + 'aws:SecureTransport': 'false' + } + } + } + ] + }; + + console.log('Updating API Gateway policy...'); + console.log(JSON.stringify(policy, null, 2)); + + // Update the API with the new policy + await apigateway.updateRestApi({ + restApiId: api.id, + patchOperations: [ + { + op: 'replace', + path: '/policy', + value: JSON.stringify(policy) + } + ] + }).promise(); + + console.log('API Gateway policy updated successfully!'); + + } catch (error) { + console.error('Error updating API policy:', error); + process.exit(1); + } +} + +main();