Skip to content

Commit 2af29d9

Browse files
authored
Merge pull request #54 from ModusCreateOrg/ADE-174-backend
POST /api/reports
2 parents dccdb97 + a52e297 commit 2af29d9

File tree

4 files changed

+176
-11
lines changed

4 files changed

+176
-11
lines changed

backend/src/iac/backend-stack.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as apigateway from 'aws-cdk-lib/aws-apigateway';
77
import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery';
88
import * as iam from 'aws-cdk-lib/aws-iam';
99
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
10+
import * as s3 from 'aws-cdk-lib/aws-s3';
1011
import * as ssm from 'aws-cdk-lib/aws-ssm';
1112

1213
import { Construct } from 'constructs';
@@ -528,6 +529,78 @@ export class BackendStack extends cdk.Stack {
528529
],
529530
});
530531

532+
// Create S3 bucket for file uploads
533+
const uploadBucket = new s3.Bucket(this, `${appName}UploadBucket-${props.environment}`, {
534+
bucketName: `${appName.toLowerCase()}-uploads-${props.environment}-${this.account}`,
535+
removalPolicy: RemovalPolicy.RETAIN,
536+
cors: [
537+
{
538+
allowedMethods: [
539+
s3.HttpMethods.GET,
540+
s3.HttpMethods.POST,
541+
s3.HttpMethods.PUT,
542+
s3.HttpMethods.DELETE,
543+
],
544+
allowedOrigins: ['*'], // In production, you should restrict this to your domain
545+
allowedHeaders: ['*'],
546+
maxAge: 3000,
547+
},
548+
],
549+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // Block all public access for security
550+
});
551+
552+
// Create a policy for authenticated users to upload files
553+
const uploadPolicy = new iam.PolicyStatement({
554+
effect: iam.Effect.ALLOW,
555+
actions: ['s3:PutObject', 's3:GetObject', 's3:DeleteObject'],
556+
resources: [`${uploadBucket.bucketArn}/*`],
557+
});
558+
559+
// Create an IAM role for authenticated users
560+
const authenticatedRole = new iam.Role(
561+
this,
562+
`${appName}AuthenticatedRole-${props.environment}`,
563+
{
564+
assumedBy: new iam.FederatedPrincipal(
565+
'cognito-identity.amazonaws.com',
566+
{
567+
StringEquals: {
568+
'cognito-identity.amazonaws.com:aud': userPool.userPoolId,
569+
},
570+
'ForAnyValue:StringLike': {
571+
'cognito-identity.amazonaws.com:amr': 'authenticated',
572+
},
573+
},
574+
'sts:AssumeRoleWithWebIdentity',
575+
),
576+
},
577+
);
578+
579+
// Attach the upload policy to the authenticated role
580+
authenticatedRole.addToPolicy(uploadPolicy);
581+
582+
// Add environment variable to the container for the S3 bucket name
583+
container.addEnvironment('S3_UPLOAD_BUCKET', uploadBucket.bucketName);
584+
585+
// Grant the task role access to the S3 bucket
586+
uploadBucket.grantReadWrite(taskRole);
587+
588+
// Add more specific S3 permissions for file processing
589+
taskRole.addToPolicy(
590+
new iam.PolicyStatement({
591+
effect: iam.Effect.ALLOW,
592+
actions: [
593+
's3:GetObject',
594+
's3:PutObject',
595+
's3:DeleteObject',
596+
's3:ListBucket',
597+
's3:GetObjectTagging',
598+
's3:PutObjectTagging',
599+
],
600+
resources: [uploadBucket.bucketArn, `${uploadBucket.bucketArn}/*`],
601+
}),
602+
);
603+
531604
// Outputs
532605
new cdk.CfnOutput(this, 'ReportsTableName', {
533606
value: reportsTable.tableName,
@@ -548,5 +621,11 @@ export class BackendStack extends cdk.Stack {
548621
value: nlb.loadBalancerDnsName,
549622
description: 'Network Load Balancer DNS Name',
550623
});
624+
625+
// Add S3 bucket name to outputs
626+
new cdk.CfnOutput(this, 'UploadBucketName', {
627+
value: uploadBucket.bucketName,
628+
description: 'S3 Bucket for file uploads',
629+
});
551630
}
552631
}

backend/src/reports/models/report.model.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,31 @@ export class Report {
99
@ApiProperty({ description: 'Unique identifier for the report' })
1010
id: string;
1111

12-
@ApiProperty({ description: 'Title of the report' })
13-
title: string;
14-
15-
@ApiProperty({ description: 'Content of the report' })
16-
content: string;
17-
1812
@ApiProperty({ description: 'User ID of the report owner' })
1913
userId: string;
2014

21-
@ApiProperty({ description: 'Creation timestamp' })
22-
createdAt: string;
15+
@ApiProperty({ description: 'Title of the report' })
16+
title: string;
2317

24-
@ApiProperty({ description: 'Last update timestamp' })
25-
updatedAt: string;
18+
@ApiProperty({ description: 'Whether the report is bookmarked' })
19+
bookmarked: boolean;
20+
21+
@ApiProperty({ description: 'Category of the report' })
22+
category: string;
2623

2724
@ApiProperty({
2825
description: 'Status of the report',
2926
enum: ReportStatus,
3027
default: ReportStatus.UNREAD,
3128
})
3229
status: ReportStatus;
30+
31+
@ApiProperty({ description: 'File path of the report' })
32+
filePath: string;
33+
34+
@ApiProperty({ description: 'Creation timestamp' })
35+
createdAt: string;
36+
37+
@ApiProperty({ description: 'Last update timestamp' })
38+
updatedAt: string;
3339
}

backend/src/reports/reports.controller.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ValidationPipe,
99
Req,
1010
UnauthorizedException,
11+
Post,
1112
} from '@nestjs/common';
1213
import {
1314
ApiTags,
@@ -16,6 +17,7 @@ import {
1617
ApiBearerAuth,
1718
ApiParam,
1819
ApiQuery,
20+
ApiBody,
1921
} from '@nestjs/swagger';
2022
import { ReportsService } from './reports.service';
2123
import { Report } from './models/report.model';
@@ -105,6 +107,34 @@ export class ReportsController {
105107
return this.reportsService.updateStatus(id, updateDto, userId);
106108
}
107109

110+
@ApiOperation({ summary: 'Create a new report from S3 file' })
111+
@ApiResponse({
112+
status: 201,
113+
description: 'Report created successfully',
114+
type: Report,
115+
})
116+
@ApiBody({
117+
schema: {
118+
type: 'object',
119+
properties: {
120+
filePath: {
121+
type: 'string',
122+
description: 'S3 file path for the report',
123+
},
124+
},
125+
required: ['filePath'],
126+
},
127+
description: 'S3 file path for the report',
128+
})
129+
@Post()
130+
async createReport(
131+
@Body('filePath') filePath: string,
132+
@Req() request: RequestWithUser,
133+
): Promise<Report> {
134+
const userId = this.extractUserId(request);
135+
return this.reportsService.saveReport(filePath, userId);
136+
}
137+
108138
private extractUserId(request: RequestWithUser): string {
109139
// The user object is attached to the request by our middleware
110140
const user = request.user;

backend/src/reports/reports.service.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
GetItemCommand,
1313
UpdateItemCommand,
1414
DynamoDBServiceException,
15+
PutItemCommand,
1516
} from '@aws-sdk/client-dynamodb';
1617
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
17-
import { Report } from './models/report.model';
18+
import { Report, ReportStatus } from './models/report.model';
1819
import { GetReportsQueryDto } from './dto/get-reports.dto';
1920
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
21+
import { v4 as uuidv4 } from 'uuid';
2022

2123
@Injectable()
2224
export class ReportsService {
@@ -250,4 +252,52 @@ export class ReportsService {
250252
throw new InternalServerErrorException(`Failed to update report status for ID ${id}`);
251253
}
252254
}
255+
256+
async saveReport(filePath: string, userId: string): Promise<Report> {
257+
if (!filePath) {
258+
throw new NotFoundException('File URL is required');
259+
}
260+
261+
if (!userId) {
262+
throw new ForbiddenException('User ID is required');
263+
}
264+
265+
try {
266+
const newReport: Report = {
267+
id: uuidv4(),
268+
userId,
269+
filePath,
270+
title: 'New Report',
271+
bookmarked: false,
272+
category: '',
273+
status: ReportStatus.UNREAD,
274+
createdAt: new Date().toISOString(),
275+
updatedAt: new Date().toISOString(),
276+
};
277+
278+
// Save to DynamoDB
279+
const command = new PutItemCommand({
280+
TableName: this.tableName,
281+
Item: marshall(newReport),
282+
});
283+
284+
await this.dynamoClient.send(command);
285+
this.logger.log(`Successfully saved report with ID ${newReport.id} for user ${userId}`);
286+
287+
return newReport;
288+
} catch (error: unknown) {
289+
this.logger.error(`Error saving report for user ${userId}:`);
290+
this.logger.error(error);
291+
292+
if (error instanceof DynamoDBServiceException) {
293+
if (error.name === 'ResourceNotFoundException') {
294+
throw new InternalServerErrorException(
295+
`Table "${this.tableName}" not found. Please check your database configuration.`,
296+
);
297+
}
298+
}
299+
300+
throw new InternalServerErrorException('Failed to save report to database');
301+
}
302+
}
253303
}

0 commit comments

Comments
 (0)