Skip to content

Commit e57b4e4

Browse files
authored
Merge branch 'main' into ADE-152
2 parents 932f8ff + 90afaa0 commit e57b4e4

File tree

7 files changed

+383
-113
lines changed

7 files changed

+383
-113
lines changed

backend/src/app.module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Module, NestModule } from '@nestjs/common';
1+
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
33
import configuration from './config/configuration';
44
import { AppController } from './app.controller';
@@ -10,6 +10,7 @@ import { PerplexityController } from './controllers/perplexity/perplexity.contro
1010
import { UserController } from './user/user.controller';
1111
import { ReportsModule } from './reports/reports.module';
1212
import { HealthController } from './health/health.controller';
13+
import { AuthMiddleware } from './auth/auth.middleware';
1314

1415
@Module({
1516
imports: [
@@ -23,8 +24,7 @@ import { HealthController } from './health/health.controller';
2324
providers: [AppService, AwsSecretsService, AwsBedrockService, PerplexityService],
2425
})
2526
export class AppModule implements NestModule {
26-
configure() {
27-
// Add your middleware configuration here if needed
28-
// If you don't need middleware, you can leave this empty
27+
configure(consumer: MiddlewareConsumer) {
28+
consumer.apply(AuthMiddleware).forRoutes('*'); // Apply to all routes
2929
}
3030
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import { Request, Response, NextFunction } from 'express';
3+
import { ConfigService } from '@nestjs/config';
4+
import * as jwt from 'jsonwebtoken';
5+
6+
// Extend the Express Request interface to include the user property
7+
export interface RequestWithUser extends Request {
8+
user?: {
9+
sub: string;
10+
email?: string;
11+
groups?: string[];
12+
[key: string]: any;
13+
} | null;
14+
}
15+
16+
// Add this interface to define the token structure
17+
interface DecodedToken {
18+
payload: {
19+
sub: string;
20+
username?: string;
21+
email?: string;
22+
[key: string]: any;
23+
};
24+
header: any;
25+
signature: string;
26+
}
27+
28+
@Injectable()
29+
export class AuthMiddleware implements NestMiddleware {
30+
constructor(private configService: ConfigService) {}
31+
32+
use(req: RequestWithUser, res: Response, next: NextFunction) {
33+
const authHeader = req.headers.authorization;
34+
if (authHeader && authHeader.startsWith('Bearer ')) {
35+
const token = authHeader.substring(7);
36+
try {
37+
// Verify the JWT token
38+
const decodedToken = jwt.decode(token, { complete: true }) as DecodedToken;
39+
40+
// Access user info from the payload
41+
req.user = {
42+
sub: decodedToken?.payload.sub as string,
43+
username: decodedToken?.payload.username as string,
44+
};
45+
} catch (error) {
46+
// If token verification fails, set user to null
47+
console.log('AuthMiddleware error');
48+
console.log(error);
49+
req.user = null;
50+
}
51+
} else {
52+
// No token provided
53+
req.user = null;
54+
}
55+
56+
next();
57+
}
58+
}

backend/src/iac/backend-stack.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,62 @@ export class BackendStack extends cdk.Stack {
445445

446446
// Add CORS to all resources
447447
api.root.addCorsPreflight(corsOptions);
448-
apiResource.addCorsPreflight(corsOptions);
449-
reportsResource.addCorsPreflight(corsOptions);
450-
latestResource.addCorsPreflight(corsOptions);
451-
reportIdResource.addCorsPreflight(corsOptions);
452-
reportStatusResource.addCorsPreflight(corsOptions);
453-
docsResource.addCorsPreflight(corsOptions);
448+
apiResource.addCorsPreflight({
449+
...corsOptions,
450+
allowCredentials: false, // This is crucial - make sure OPTIONS requests don't require credentials
451+
});
452+
reportsResource.addCorsPreflight({
453+
...corsOptions,
454+
allowCredentials: false,
455+
});
456+
latestResource.addCorsPreflight({
457+
...corsOptions,
458+
allowCredentials: false,
459+
});
460+
reportIdResource.addCorsPreflight({
461+
...corsOptions,
462+
allowCredentials: false,
463+
});
464+
reportStatusResource.addCorsPreflight({
465+
...corsOptions,
466+
allowCredentials: false,
467+
});
468+
docsResource.addCorsPreflight({
469+
...corsOptions,
470+
allowCredentials: false,
471+
});
472+
473+
// Configure Gateway Responses to add CORS headers to error responses
474+
const gatewayResponseTypes = [
475+
apigateway.ResponseType.UNAUTHORIZED,
476+
apigateway.ResponseType.ACCESS_DENIED,
477+
apigateway.ResponseType.DEFAULT_4XX,
478+
apigateway.ResponseType.DEFAULT_5XX,
479+
apigateway.ResponseType.RESOURCE_NOT_FOUND,
480+
apigateway.ResponseType.MISSING_AUTHENTICATION_TOKEN,
481+
apigateway.ResponseType.INVALID_API_KEY,
482+
apigateway.ResponseType.THROTTLED,
483+
apigateway.ResponseType.INTEGRATION_FAILURE,
484+
apigateway.ResponseType.INTEGRATION_TIMEOUT,
485+
];
486+
487+
gatewayResponseTypes.forEach(responseType => {
488+
new apigateway.CfnGatewayResponse(
489+
this,
490+
`${appName}GatewayResponse-${responseType.responseType.toString()}-${props.environment}`,
491+
{
492+
restApiId: api.restApiId,
493+
responseType: responseType.responseType.toString(),
494+
responseParameters: {
495+
'gatewayresponse.header.Access-Control-Allow-Origin': "'*'",
496+
'gatewayresponse.header.Access-Control-Allow-Headers':
497+
"'Content-Type,Authorization,X-Amz-Date,X-Api-Key'",
498+
'gatewayresponse.header.Access-Control-Allow-Methods':
499+
"'GET,POST,PUT,PATCH,DELETE,OPTIONS'",
500+
},
501+
},
502+
);
503+
});
454504

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

backend/src/iac/update-api-policy.js

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,29 +62,36 @@ async function main() {
6262
const policy = {
6363
Version: '2012-10-17',
6464
Statement: [
65-
// Allow authenticated Cognito users
6665
{
67-
Effect: 'Allow',
68-
Principal: '*',
69-
Action: 'execute-api:Invoke',
70-
Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`,
71-
Condition: {
72-
StringEquals: {
73-
'cognito-identity.amazonaws.com:aud': cognitoUserPoolId
66+
"Version": "2012-10-17",
67+
"Statement": [
68+
// Allow OPTIONS requests
69+
{
70+
"Effect": "Allow",
71+
"Principal": "*",
72+
"Action": "execute-api:Invoke",
73+
"Resource": "arn:aws:execute-api:us-east-1:*:xhvwo6wp66/*/OPTIONS/*"
74+
},
75+
{
76+
// Allow all other requests - authentication will be handled by Cognito
77+
"Effect": "Allow",
78+
"Principal": "*",
79+
"Action": "execute-api:Invoke",
80+
"Resource": "arn:aws:execute-api:us-east-1:*:xhvwo6wp66/*/*"
81+
},
82+
{
83+
// Deny non-HTTPS requests
84+
"Effect": "Deny",
85+
"Principal": "*",
86+
"Action": "execute-api:Invoke",
87+
"Resource": "arn:aws:execute-api:us-east-1:*:xhvwo6wp66/*/*",
88+
"Condition": {
89+
"Bool": {
90+
"aws:SecureTransport": "false"
91+
}
92+
}
7493
}
75-
}
76-
},
77-
// Deny non-HTTPS requests
78-
{
79-
Effect: 'Deny',
80-
Principal: '*',
81-
Action: 'execute-api:Invoke',
82-
Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`,
83-
Condition: {
84-
Bool: {
85-
'aws:SecureTransport': 'false'
86-
}
87-
}
94+
]
8895
}
8996
]
9097
};

backend/src/main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ async function bootstrap() {
1313
// Enable CORS
1414
app.enableCors({
1515
origin: [
16-
'http://localhost:5173', // Vite default dev server
16+
'http://localhost:5173',
1717
'http://localhost:3000',
18-
'http://localhost:4173', // Vite preview
18+
'http://localhost:4173',
19+
'https://localhost', // Add this for Capacitor
1920
...(process.env.FRONTEND_URL ? [process.env.FRONTEND_URL] : []),
2021
],
2122
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',

backend/src/reports/reports.controller.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
import { Controller, Get, Patch, Param, Body, Query, ValidationPipe } from '@nestjs/common';
1+
import {
2+
Controller,
3+
Get,
4+
Patch,
5+
Param,
6+
Body,
7+
Query,
8+
ValidationPipe,
9+
Req,
10+
UnauthorizedException,
11+
} from '@nestjs/common';
212
import {
313
ApiTags,
414
ApiOperation,
@@ -11,6 +21,7 @@ import { ReportsService } from './reports.service';
1121
import { Report } from './models/report.model';
1222
import { GetReportsQueryDto } from './dto/get-reports.dto';
1323
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
24+
import { RequestWithUser } from '../auth/auth.middleware';
1425

1526
@ApiTags('reports')
1627
@Controller('reports')
@@ -21,18 +32,19 @@ export class ReportsController {
2132
@ApiOperation({ summary: 'Get all reports' })
2233
@ApiResponse({
2334
status: 200,
24-
description: 'Returns all reports',
35+
description: 'Returns all reports for the authenticated user',
2536
type: [Report],
2637
})
2738
@Get()
28-
async findAll(): Promise<Report[]> {
29-
return this.reportsService.findAll();
39+
async findAll(@Req() request: RequestWithUser): Promise<Report[]> {
40+
const userId = this.extractUserId(request);
41+
return this.reportsService.findAll(userId);
3042
}
3143

3244
@ApiOperation({ summary: 'Get latest reports' })
3345
@ApiResponse({
3446
status: 200,
35-
description: 'Returns the latest reports',
47+
description: 'Returns the latest reports for the authenticated user',
3648
type: [Report],
3749
})
3850
@ApiQuery({
@@ -41,14 +53,18 @@ export class ReportsController {
4153
description: 'Maximum number of reports to return',
4254
})
4355
@Get('latest')
44-
async findLatest(@Query(ValidationPipe) queryDto: GetReportsQueryDto): Promise<Report[]> {
45-
return this.reportsService.findLatest(queryDto);
56+
async findLatest(
57+
@Query(ValidationPipe) queryDto: GetReportsQueryDto,
58+
@Req() request: RequestWithUser,
59+
): Promise<Report[]> {
60+
const userId = this.extractUserId(request);
61+
return this.reportsService.findLatest(queryDto, userId);
4662
}
4763

4864
@ApiOperation({ summary: 'GET report' })
4965
@ApiResponse({
5066
status: 200,
51-
description: 'Report status updated successfully',
67+
description: 'Report details',
5268
type: Report,
5369
})
5470
@ApiResponse({
@@ -60,8 +76,9 @@ export class ReportsController {
6076
description: 'Report ID',
6177
})
6278
@Get(':id')
63-
async getReport(@Param('id') id: string): Promise<Report> {
64-
return this.reportsService.findOne(id);
79+
async getReport(@Param('id') id: string, @Req() request: RequestWithUser): Promise<Report> {
80+
const userId = this.extractUserId(request);
81+
return this.reportsService.findOne(id, userId);
6582
}
6683

6784
@ApiOperation({ summary: 'Update report status' })
@@ -82,7 +99,20 @@ export class ReportsController {
8299
async updateStatus(
83100
@Param('id') id: string,
84101
@Body(ValidationPipe) updateDto: UpdateReportStatusDto,
102+
@Req() request: RequestWithUser,
85103
): Promise<Report> {
86-
return this.reportsService.updateStatus(id, updateDto);
104+
const userId = this.extractUserId(request);
105+
return this.reportsService.updateStatus(id, updateDto, userId);
106+
}
107+
108+
private extractUserId(request: RequestWithUser): string {
109+
// The user object is attached to the request by our middleware
110+
const user = request.user;
111+
112+
if (!user || !user.sub) {
113+
throw new UnauthorizedException('User ID not found in request');
114+
}
115+
116+
return user.sub;
87117
}
88118
}

0 commit comments

Comments
 (0)