From 95737311bc4d44fe1ddc1bd080c708c96defb22a Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 18:11:08 -0400 Subject: [PATCH 1/4] Add Execution Task Role with access to DynamoDB to ECS Fargate --- backend/src/iac/backend-stack.ts | 40 ++++++++++++++++++++++---- backend/src/reports/reports.service.ts | 1 + 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index b5fb3645..cd274097 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -125,19 +125,49 @@ export class BackendStack extends cdk.Stack { 'Allow inbound HTTPS traffic from within VPC', ); - // Task Definition + // Create Task Execution Role - this is used during task startup + const taskExecutionRole = new iam.Role(this, `${appName}TaskExecutionRole-${props.environment}`, { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + description: 'Role that the ECS service uses to pull container images and publish logs to CloudWatch', + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy') + ] + }); + + // Create Task Role - this is used by the container during runtime + const taskRole = new iam.Role(this, `${appName}TaskRole-${props.environment}`, { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + description: 'Role that the containers in the task assume', + }); + + // Grant permissions to the task role + // DynamoDB permissions + reportsTable.grantReadWriteData(taskRole); + + // Add permission to read Perplexity API key from Secrets Manager + taskRole.addToPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret' + ], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:medical-reports-explainer/${props.environment}/perplexity-api-key-*` + ] + })); + + // Task Definition with explicit roles const taskDefinition = new ecs.FargateTaskDefinition( this, `${appName}TaskDef-${props.environment}`, { memoryLimitMiB: isProd ? 1024 : 512, cpu: isProd ? 512 : 256, + taskRole: taskRole, // Role that the application uses to call AWS services + executionRole: taskExecutionRole // Role that ECS uses to pull images and write logs }, ); - // 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, @@ -194,7 +224,7 @@ export class BackendStack extends cdk.Stack { }); // Grant the task role access to read the SSL certificate secret - certificateSecret.grantRead(taskDefinition.taskRole); + certificateSecret.grantRead(taskRole); container.addPortMappings({ containerPort: 3000, diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index efa1a01b..7f7ae201 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -61,6 +61,7 @@ export class ReportsService { } async findLatest(queryDto: GetReportsQueryDto): Promise { + console.log('Running ReportsService.findLatest', queryDto); // Convert limit to a number to avoid serialization errors const limit = typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10; From 31d8bb5e62f18a12113ee6bc9c6d2b1c8e487cde Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 18:11:29 -0400 Subject: [PATCH 2/4] Add Execution Task Role with access to DynamoDB to ECS Fargate --- backend/src/iac/backend-stack.ts | 44 ++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index cd274097..77c87e32 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -126,13 +126,20 @@ export class BackendStack extends cdk.Stack { ); // Create Task Execution Role - this is used during task startup - const taskExecutionRole = new iam.Role(this, `${appName}TaskExecutionRole-${props.environment}`, { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - description: 'Role that the ECS service uses to pull container images and publish logs to CloudWatch', - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy') - ] - }); + const taskExecutionRole = new iam.Role( + this, + `${appName}TaskExecutionRole-${props.environment}`, + { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + description: + 'Role that the ECS service uses to pull container images and publish logs to CloudWatch', + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AmazonECSTaskExecutionRolePolicy', + ), + ], + }, + ); // Create Task Role - this is used by the container during runtime const taskRole = new iam.Role(this, `${appName}TaskRole-${props.environment}`, { @@ -145,16 +152,15 @@ export class BackendStack extends cdk.Stack { reportsTable.grantReadWriteData(taskRole); // Add permission to read Perplexity API key from Secrets Manager - taskRole.addToPolicy(new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - 'secretsmanager:GetSecretValue', - 'secretsmanager:DescribeSecret' - ], - resources: [ - `arn:aws:secretsmanager:${this.region}:${this.account}:secret:medical-reports-explainer/${props.environment}/perplexity-api-key-*` - ] - })); + taskRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + resources: [ + `arn:aws:secretsmanager:${this.region}:${this.account}:secret:medical-reports-explainer/${props.environment}/perplexity-api-key-*`, + ], + }), + ); // Task Definition with explicit roles const taskDefinition = new ecs.FargateTaskDefinition( @@ -163,8 +169,8 @@ export class BackendStack extends cdk.Stack { { memoryLimitMiB: isProd ? 1024 : 512, cpu: isProd ? 512 : 256, - taskRole: taskRole, // Role that the application uses to call AWS services - executionRole: taskExecutionRole // Role that ECS uses to pull images and write logs + taskRole: taskRole, // Role that the application uses to call AWS services + executionRole: taskExecutionRole, // Role that ECS uses to pull images and write logs }, ); From cd263ac21079b0a449e88c68d903a019d4764cb9 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 18:57:31 -0400 Subject: [PATCH 3/4] Add /docs endpoint --- backend/src/iac/backend-stack.ts | 38 ++++++++++---------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 77c87e32..d19f6a55 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -343,6 +343,9 @@ export class BackendStack extends cdk.Stack { // Create the 'api' resource const apiResource = api.root.addResource('api'); + // Create the 'docs' resource under 'api' + const docsResource = apiResource.addResource('docs'); + // Create the 'reports' resource under 'api' const reportsResource = apiResource.addResource('reports'); @@ -361,6 +364,13 @@ export class BackendStack extends cdk.Stack { vpcLink: vpcLink, }; + const getDocsIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/docs`, + options: integrationOptions, + }); + // Create integrations for each endpoint const getReportsIntegration = new apigateway.Integration({ type: apigateway.IntegrationType.HTTP_PROXY, @@ -409,7 +419,7 @@ export class BackendStack extends cdk.Stack { // Add methods to the resources reportsResource.addMethod('GET', getReportsIntegration, methodOptions); latestResource.addMethod('GET', getLatestReportIntegration, methodOptions); - + docsResource.addMethod('GET', getDocsIntegration, methodOptions); // For path parameter methods, add the request parameter configuration reportIdResource.addMethod('GET', getReportByIdIntegration, { ...methodOptions, @@ -440,31 +450,7 @@ export class BackendStack extends cdk.Stack { 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', - // }, - // }, - // }), - // ], - // }); + docsResource.addCorsPreflight(corsOptions); // Create API Gateway execution role with required permissions new iam.Role(this, `${appName}APIGatewayRole-${props.environment}`, { From 421d355e813bdbcbb971b5af1938fbbbccd3b3b1 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 19:38:03 -0400 Subject: [PATCH 4/4] Add auth token to the frontend --- frontend/src/common/api/reportService.ts | 42 +++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/frontend/src/common/api/reportService.ts b/frontend/src/common/api/reportService.ts index c0626a37..1ce21ff4 100644 --- a/frontend/src/common/api/reportService.ts +++ b/frontend/src/common/api/reportService.ts @@ -1,6 +1,6 @@ import axios, { AxiosProgressEvent, AxiosRequestConfig } from 'axios'; import { MedicalReport, ReportStatus, ReportCategory } from '../models/medicalReport'; - +import { fetchAuthSession } from '@aws-amplify/auth'; // Get the API URL from environment variables const API_URL = import.meta.env.VITE_BASE_URL_API || ''; @@ -15,6 +15,18 @@ const mockReports: MedicalReport[] = [ } ]; +/** + * Creates an authenticated request config with bearer token + */ +const getAuthConfig = async (): Promise => { + const session = await fetchAuthSession(); + return { + headers: { + Authorization: session.tokens?.idToken ? `Bearer ${session.tokens.idToken.toString()}` : '' + } + }; +}; + /** * Error thrown when report operations fail. */ @@ -58,32 +70,32 @@ export const uploadReport = async ( // Create form data for file upload const formData = new FormData(); formData.append('file', file); - + // Optional metadata about the file formData.append('fileName', file.name); formData.append('fileType', file.type); formData.append('fileSize', file.size.toString()); - + // Setup request config with progress tracking if callback provided const config: AxiosRequestConfig = { headers: { 'Content-Type': 'multipart/form-data' } }; - + if (onProgress) { config.onUploadProgress = (progressEvent: AxiosProgressEvent) => { - const percentCompleted = progressEvent.total + const percentCompleted = progressEvent.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) / 100 : 0; onProgress(percentCompleted); }; } - + // In a real app, this would be an actual API call // const response = await axios.post('/api/reports/upload', formData, config); // return response.data; - + // For demonstration purposes, simulate upload delay and return mock data await new Promise(resolve => { // Simulate progress updates @@ -97,7 +109,7 @@ export const uploadReport = async ( } onProgress(progress); }, 200); - + // Resolve after simulated upload time setTimeout(() => { clearInterval(interval); @@ -109,7 +121,7 @@ export const uploadReport = async ( setTimeout(resolve, 2000); } }); - + // Create a new report based on the uploaded file const newReport: MedicalReport = { id: String(mockReports.length + 1), @@ -119,10 +131,10 @@ export const uploadReport = async ( status: ReportStatus.UNREAD, documentUrl: `https://example.com/reports/${mockReports.length + 1}/${file.name}` // Mock URL }; - + // Add to mock data mockReports.unshift(newReport); - + return newReport; } catch (error) { if (axios.isAxiosError(error)) { @@ -139,8 +151,8 @@ export const uploadReport = async ( */ const determineCategory = (filename: string): ReportCategory => { const lowerFilename = filename.toLowerCase(); - - const matchedCategory = Object.entries(CATEGORY_KEYWORDS).find(([_, keywords]) => + + const matchedCategory = Object.entries(CATEGORY_KEYWORDS).find(([_, keywords]) => keywords.some(keyword => lowerFilename.includes(keyword)) ); @@ -154,7 +166,7 @@ const determineCategory = (filename: string): ReportCategory => { */ export const fetchLatestReports = async (limit = 3): Promise => { try { - const response = await axios.get(`${API_URL}/api/reports/latest?limit=${limit}`); + const response = await axios.get(`${API_URL}/api/reports/latest?limit=${limit}`, await getAuthConfig()); console.log('response', response.data); console.log('API_URL', API_URL); return response.data; @@ -172,7 +184,7 @@ export const fetchLatestReports = async (limit = 3): Promise => */ export const fetchAllReports = async (): Promise => { try { - const response = await axios.get(`${API_URL}/api/reports`); + const response = await axios.get(`${API_URL}/api/reports`, await getAuthConfig() ); return response.data; } catch (error) { if (axios.isAxiosError(error)) {