Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 53 additions & 31 deletions backend/src/iac/backend-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,55 @@ 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,
Expand Down Expand Up @@ -194,7 +230,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,
Expand Down Expand Up @@ -307,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');

Expand All @@ -325,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,
Expand Down Expand Up @@ -373,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,
Expand Down Expand Up @@ -404,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}`, {
Expand Down
1 change: 1 addition & 0 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class ReportsService {
}

async findLatest(queryDto: GetReportsQueryDto): Promise<Report[]> {
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;
Expand Down
42 changes: 27 additions & 15 deletions frontend/src/common/api/reportService.ts
Original file line number Diff line number Diff line change
@@ -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 || '';

Expand All @@ -15,6 +15,18 @@ const mockReports: MedicalReport[] = [
}
];

/**
* Creates an authenticated request config with bearer token
*/
const getAuthConfig = async (): Promise<AxiosRequestConfig> => {
const session = await fetchAuthSession();
return {
headers: {
Authorization: session.tokens?.idToken ? `Bearer ${session.tokens.idToken.toString()}` : ''
}
};
};

/**
* Error thrown when report operations fail.
*/
Expand Down Expand Up @@ -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<void>(resolve => {
// Simulate progress updates
Expand All @@ -97,7 +109,7 @@ export const uploadReport = async (
}
onProgress(progress);
}, 200);

// Resolve after simulated upload time
setTimeout(() => {
clearInterval(interval);
Expand All @@ -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),
Expand All @@ -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)) {
Expand All @@ -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))
);

Expand All @@ -154,7 +166,7 @@ const determineCategory = (filename: string): ReportCategory => {
*/
export const fetchLatestReports = async (limit = 3): Promise<MedicalReport[]> => {
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;
Expand All @@ -172,7 +184,7 @@ export const fetchLatestReports = async (limit = 3): Promise<MedicalReport[]> =>
*/
export const fetchAllReports = async (): Promise<MedicalReport[]> => {
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)) {
Expand Down