Skip to content

Commit 40de18c

Browse files
Merge pull request #42 from ModusCreateOrg/ADE-150-API-configuration
ADE-150 API Configuration
2 parents f3cec95 + 421d355 commit 40de18c

File tree

3 files changed

+81
-46
lines changed

3 files changed

+81
-46
lines changed

backend/src/iac/backend-stack.ts

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,55 @@ export class BackendStack extends cdk.Stack {
125125
'Allow inbound HTTPS traffic from within VPC',
126126
);
127127

128-
// Task Definition
128+
// Create Task Execution Role - this is used during task startup
129+
const taskExecutionRole = new iam.Role(
130+
this,
131+
`${appName}TaskExecutionRole-${props.environment}`,
132+
{
133+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
134+
description:
135+
'Role that the ECS service uses to pull container images and publish logs to CloudWatch',
136+
managedPolicies: [
137+
iam.ManagedPolicy.fromAwsManagedPolicyName(
138+
'service-role/AmazonECSTaskExecutionRolePolicy',
139+
),
140+
],
141+
},
142+
);
143+
144+
// Create Task Role - this is used by the container during runtime
145+
const taskRole = new iam.Role(this, `${appName}TaskRole-${props.environment}`, {
146+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
147+
description: 'Role that the containers in the task assume',
148+
});
149+
150+
// Grant permissions to the task role
151+
// DynamoDB permissions
152+
reportsTable.grantReadWriteData(taskRole);
153+
154+
// Add permission to read Perplexity API key from Secrets Manager
155+
taskRole.addToPolicy(
156+
new iam.PolicyStatement({
157+
effect: iam.Effect.ALLOW,
158+
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
159+
resources: [
160+
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:medical-reports-explainer/${props.environment}/perplexity-api-key-*`,
161+
],
162+
}),
163+
);
164+
165+
// Task Definition with explicit roles
129166
const taskDefinition = new ecs.FargateTaskDefinition(
130167
this,
131168
`${appName}TaskDef-${props.environment}`,
132169
{
133170
memoryLimitMiB: isProd ? 1024 : 512,
134171
cpu: isProd ? 512 : 256,
172+
taskRole: taskRole, // Role that the application uses to call AWS services
173+
executionRole: taskExecutionRole, // Role that ECS uses to pull images and write logs
135174
},
136175
);
137176

138-
// Grant DynamoDB permissions to task
139-
reportsTable.grantReadWriteData(taskDefinition.taskRole);
140-
141177
// Create a secrets manager for the SSL certificate and key
142178
const certificateSecret = new cdk.aws_secretsmanager.Secret(
143179
this,
@@ -194,7 +230,7 @@ export class BackendStack extends cdk.Stack {
194230
});
195231

196232
// Grant the task role access to read the SSL certificate secret
197-
certificateSecret.grantRead(taskDefinition.taskRole);
233+
certificateSecret.grantRead(taskRole);
198234

199235
container.addPortMappings({
200236
containerPort: 3000,
@@ -307,6 +343,9 @@ export class BackendStack extends cdk.Stack {
307343
// Create the 'api' resource
308344
const apiResource = api.root.addResource('api');
309345

346+
// Create the 'docs' resource under 'api'
347+
const docsResource = apiResource.addResource('docs');
348+
310349
// Create the 'reports' resource under 'api'
311350
const reportsResource = apiResource.addResource('reports');
312351

@@ -325,6 +364,13 @@ export class BackendStack extends cdk.Stack {
325364
vpcLink: vpcLink,
326365
};
327366

367+
const getDocsIntegration = new apigateway.Integration({
368+
type: apigateway.IntegrationType.HTTP_PROXY,
369+
integrationHttpMethod: 'GET',
370+
uri: `${serviceUrl}/api/docs`,
371+
options: integrationOptions,
372+
});
373+
328374
// Create integrations for each endpoint
329375
const getReportsIntegration = new apigateway.Integration({
330376
type: apigateway.IntegrationType.HTTP_PROXY,
@@ -373,7 +419,7 @@ export class BackendStack extends cdk.Stack {
373419
// Add methods to the resources
374420
reportsResource.addMethod('GET', getReportsIntegration, methodOptions);
375421
latestResource.addMethod('GET', getLatestReportIntegration, methodOptions);
376-
422+
docsResource.addMethod('GET', getDocsIntegration, methodOptions);
377423
// For path parameter methods, add the request parameter configuration
378424
reportIdResource.addMethod('GET', getReportByIdIntegration, {
379425
...methodOptions,
@@ -404,31 +450,7 @@ export class BackendStack extends cdk.Stack {
404450
latestResource.addCorsPreflight(corsOptions);
405451
reportIdResource.addCorsPreflight(corsOptions);
406452
reportStatusResource.addCorsPreflight(corsOptions);
407-
408-
// Apply resource policy separately after resources and methods are created
409-
// const apiResourcePolicy = new iam.PolicyDocument({
410-
// statements: [
411-
// // Allow authenticated Cognito users
412-
// new iam.PolicyStatement({
413-
// effect: iam.Effect.ALLOW,
414-
// principals: [new iam.AnyPrincipal()],
415-
// actions: ['execute-api:Invoke'],
416-
// resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`],
417-
// }),
418-
// // Deny non-HTTPS requests
419-
// new iam.PolicyStatement({
420-
// effect: iam.Effect.DENY,
421-
// principals: [new iam.AnyPrincipal()],
422-
// actions: ['execute-api:Invoke'],
423-
// resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`],
424-
// conditions: {
425-
// Bool: {
426-
// 'aws:SecureTransport': 'false',
427-
// },
428-
// },
429-
// }),
430-
// ],
431-
// });
453+
docsResource.addCorsPreflight(corsOptions);
432454

433455
// Create API Gateway execution role with required permissions
434456
new iam.Role(this, `${appName}APIGatewayRole-${props.environment}`, {

backend/src/reports/reports.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class ReportsService {
6161
}
6262

6363
async findLatest(queryDto: GetReportsQueryDto): Promise<Report[]> {
64+
console.log('Running ReportsService.findLatest', queryDto);
6465
// Convert limit to a number to avoid serialization errors
6566
const limit =
6667
typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10;

frontend/src/common/api/reportService.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios, { AxiosProgressEvent, AxiosRequestConfig } from 'axios';
22
import { MedicalReport, ReportStatus, ReportCategory } from '../models/medicalReport';
3-
3+
import { fetchAuthSession } from '@aws-amplify/auth';
44
// Get the API URL from environment variables
55
const API_URL = import.meta.env.VITE_BASE_URL_API || '';
66

@@ -15,6 +15,18 @@ const mockReports: MedicalReport[] = [
1515
}
1616
];
1717

18+
/**
19+
* Creates an authenticated request config with bearer token
20+
*/
21+
const getAuthConfig = async (): Promise<AxiosRequestConfig> => {
22+
const session = await fetchAuthSession();
23+
return {
24+
headers: {
25+
Authorization: session.tokens?.idToken ? `Bearer ${session.tokens.idToken.toString()}` : ''
26+
}
27+
};
28+
};
29+
1830
/**
1931
* Error thrown when report operations fail.
2032
*/
@@ -58,32 +70,32 @@ export const uploadReport = async (
5870
// Create form data for file upload
5971
const formData = new FormData();
6072
formData.append('file', file);
61-
73+
6274
// Optional metadata about the file
6375
formData.append('fileName', file.name);
6476
formData.append('fileType', file.type);
6577
formData.append('fileSize', file.size.toString());
66-
78+
6779
// Setup request config with progress tracking if callback provided
6880
const config: AxiosRequestConfig = {
6981
headers: {
7082
'Content-Type': 'multipart/form-data'
7183
}
7284
};
73-
85+
7486
if (onProgress) {
7587
config.onUploadProgress = (progressEvent: AxiosProgressEvent) => {
76-
const percentCompleted = progressEvent.total
88+
const percentCompleted = progressEvent.total
7789
? Math.round((progressEvent.loaded * 100) / progressEvent.total) / 100
7890
: 0;
7991
onProgress(percentCompleted);
8092
};
8193
}
82-
94+
8395
// In a real app, this would be an actual API call
8496
// const response = await axios.post('/api/reports/upload', formData, config);
8597
// return response.data;
86-
98+
8799
// For demonstration purposes, simulate upload delay and return mock data
88100
await new Promise<void>(resolve => {
89101
// Simulate progress updates
@@ -97,7 +109,7 @@ export const uploadReport = async (
97109
}
98110
onProgress(progress);
99111
}, 200);
100-
112+
101113
// Resolve after simulated upload time
102114
setTimeout(() => {
103115
clearInterval(interval);
@@ -109,7 +121,7 @@ export const uploadReport = async (
109121
setTimeout(resolve, 2000);
110122
}
111123
});
112-
124+
113125
// Create a new report based on the uploaded file
114126
const newReport: MedicalReport = {
115127
id: String(mockReports.length + 1),
@@ -119,10 +131,10 @@ export const uploadReport = async (
119131
status: ReportStatus.UNREAD,
120132
documentUrl: `https://example.com/reports/${mockReports.length + 1}/${file.name}` // Mock URL
121133
};
122-
134+
123135
// Add to mock data
124136
mockReports.unshift(newReport);
125-
137+
126138
return newReport;
127139
} catch (error) {
128140
if (axios.isAxiosError(error)) {
@@ -139,8 +151,8 @@ export const uploadReport = async (
139151
*/
140152
const determineCategory = (filename: string): ReportCategory => {
141153
const lowerFilename = filename.toLowerCase();
142-
143-
const matchedCategory = Object.entries(CATEGORY_KEYWORDS).find(([_, keywords]) =>
154+
155+
const matchedCategory = Object.entries(CATEGORY_KEYWORDS).find(([_, keywords]) =>
144156
keywords.some(keyword => lowerFilename.includes(keyword))
145157
);
146158

@@ -154,7 +166,7 @@ const determineCategory = (filename: string): ReportCategory => {
154166
*/
155167
export const fetchLatestReports = async (limit = 3): Promise<MedicalReport[]> => {
156168
try {
157-
const response = await axios.get(`${API_URL}/api/reports/latest?limit=${limit}`);
169+
const response = await axios.get(`${API_URL}/api/reports/latest?limit=${limit}`, await getAuthConfig());
158170
console.log('response', response.data);
159171
console.log('API_URL', API_URL);
160172
return response.data;
@@ -172,7 +184,7 @@ export const fetchLatestReports = async (limit = 3): Promise<MedicalReport[]> =>
172184
*/
173185
export const fetchAllReports = async (): Promise<MedicalReport[]> => {
174186
try {
175-
const response = await axios.get(`${API_URL}/api/reports`);
187+
const response = await axios.get(`${API_URL}/api/reports`, await getAuthConfig() );
176188
return response.data;
177189
} catch (error) {
178190
if (axios.isAxiosError(error)) {

0 commit comments

Comments
 (0)