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
9 changes: 5 additions & 4 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, NestModule } from '@nestjs/common';
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
import { AppController } from './app.controller';
Expand All @@ -9,6 +9,8 @@ import { PerplexityController } from './controllers/perplexity/perplexity.contro
import { UserController } from './user/user.controller';
import { ReportsModule } from './reports/reports.module';
import { HealthController } from './health/health.controller';
import { AuthMiddleware } from './auth/auth.middleware';

@Module({
imports: [
ConfigModule.forRoot({
Expand All @@ -21,8 +23,7 @@ import { HealthController } from './health/health.controller';
providers: [AppService, AwsSecretsService, PerplexityService],
})
export class AppModule implements NestModule {
configure() {
// Add your middleware configuration here if needed
// If you don't need middleware, you can leave this empty
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes('*'); // Apply to all routes
}
}
58 changes: 58 additions & 0 deletions backend/src/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';
import * as jwt from 'jsonwebtoken';

// Extend the Express Request interface to include the user property
export interface RequestWithUser extends Request {
user?: {
sub: string;
email?: string;
groups?: string[];
[key: string]: any;
} | null;
}

// Add this interface to define the token structure
interface DecodedToken {
payload: {
sub: string;
username?: string;
email?: string;
[key: string]: any;
};
header: any;
signature: string;
}

@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private configService: ConfigService) {}

use(req: RequestWithUser, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
// Verify the JWT token
const decodedToken = jwt.decode(token, { complete: true }) as DecodedToken;

// Access user info from the payload
req.user = {
sub: decodedToken?.payload.sub as string,
username: decodedToken?.payload.username as string,
};
} catch (error) {
// If token verification fails, set user to null
console.log('AuthMiddleware error');
console.log(error);
req.user = null;
}
} else {
// No token provided
req.user = null;
}

next();
}
}
62 changes: 56 additions & 6 deletions backend/src/iac/backend-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,62 @@ export class BackendStack extends cdk.Stack {

// Add CORS to all resources
api.root.addCorsPreflight(corsOptions);
apiResource.addCorsPreflight(corsOptions);
reportsResource.addCorsPreflight(corsOptions);
latestResource.addCorsPreflight(corsOptions);
reportIdResource.addCorsPreflight(corsOptions);
reportStatusResource.addCorsPreflight(corsOptions);
docsResource.addCorsPreflight(corsOptions);
apiResource.addCorsPreflight({
...corsOptions,
allowCredentials: false, // This is crucial - make sure OPTIONS requests don't require credentials
});
reportsResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
});
latestResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
});
reportIdResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
});
reportStatusResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
});
docsResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
});

// Configure Gateway Responses to add CORS headers to error responses
const gatewayResponseTypes = [
apigateway.ResponseType.UNAUTHORIZED,
apigateway.ResponseType.ACCESS_DENIED,
apigateway.ResponseType.DEFAULT_4XX,
apigateway.ResponseType.DEFAULT_5XX,
apigateway.ResponseType.RESOURCE_NOT_FOUND,
apigateway.ResponseType.MISSING_AUTHENTICATION_TOKEN,
apigateway.ResponseType.INVALID_API_KEY,
apigateway.ResponseType.THROTTLED,
apigateway.ResponseType.INTEGRATION_FAILURE,
apigateway.ResponseType.INTEGRATION_TIMEOUT,
];

gatewayResponseTypes.forEach(responseType => {
new apigateway.CfnGatewayResponse(
this,
`${appName}GatewayResponse-${responseType.responseType.toString()}-${props.environment}`,
{
restApiId: api.restApiId,
responseType: responseType.responseType.toString(),
responseParameters: {
'gatewayresponse.header.Access-Control-Allow-Origin': "'*'",
'gatewayresponse.header.Access-Control-Allow-Headers':
"'Content-Type,Authorization,X-Amz-Date,X-Api-Key'",
'gatewayresponse.header.Access-Control-Allow-Methods':
"'GET,POST,PUT,PATCH,DELETE,OPTIONS'",
},
},
);
});

// Create API Gateway execution role with required permissions
new iam.Role(this, `${appName}APIGatewayRole-${props.environment}`, {
Expand Down
49 changes: 28 additions & 21 deletions backend/src/iac/update-api-policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,36 @@ async function main() {
const policy = {
Version: '2012-10-17',
Statement: [
// Allow authenticated Cognito users
{
Effect: 'Allow',
Principal: '*',
Action: 'execute-api:Invoke',
Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`,
Condition: {
StringEquals: {
'cognito-identity.amazonaws.com:aud': cognitoUserPoolId
"Version": "2012-10-17",
"Statement": [
// Allow OPTIONS requests
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-east-1:*:xhvwo6wp66/*/OPTIONS/*"
},
{
// Allow all other requests - authentication will be handled by Cognito
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-east-1:*:xhvwo6wp66/*/*"
},
{
// Deny non-HTTPS requests
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-east-1:*:xhvwo6wp66/*/*",
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
}
},
// Deny non-HTTPS requests
{
Effect: 'Deny',
Principal: '*',
Action: 'execute-api:Invoke',
Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`,
Condition: {
Bool: {
'aws:SecureTransport': 'false'
}
}
]
}
]
};
Expand Down
5 changes: 3 additions & 2 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ async function bootstrap() {
// Enable CORS
app.enableCors({
origin: [
'http://localhost:5173', // Vite default dev server
'http://localhost:5173',
'http://localhost:3000',
'http://localhost:4173', // Vite preview
'http://localhost:4173',
'https://localhost', // Add this for Capacitor
...(process.env.FRONTEND_URL ? [process.env.FRONTEND_URL] : []),
],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
Expand Down
52 changes: 41 additions & 11 deletions backend/src/reports/reports.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Controller, Get, Patch, Param, Body, Query, ValidationPipe } from '@nestjs/common';
import {
Controller,
Get,
Patch,
Param,
Body,
Query,
ValidationPipe,
Req,
UnauthorizedException,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
Expand All @@ -11,6 +21,7 @@ import { ReportsService } from './reports.service';
import { Report } from './models/report.model';
import { GetReportsQueryDto } from './dto/get-reports.dto';
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
import { RequestWithUser } from '../auth/auth.middleware';

@ApiTags('reports')
@Controller('reports')
Expand All @@ -21,18 +32,19 @@ export class ReportsController {
@ApiOperation({ summary: 'Get all reports' })
@ApiResponse({
status: 200,
description: 'Returns all reports',
description: 'Returns all reports for the authenticated user',
type: [Report],
})
@Get()
async findAll(): Promise<Report[]> {
return this.reportsService.findAll();
async findAll(@Req() request: RequestWithUser): Promise<Report[]> {
const userId = this.extractUserId(request);
return this.reportsService.findAll(userId);
}

@ApiOperation({ summary: 'Get latest reports' })
@ApiResponse({
status: 200,
description: 'Returns the latest reports',
description: 'Returns the latest reports for the authenticated user',
type: [Report],
})
@ApiQuery({
Expand All @@ -41,14 +53,18 @@ export class ReportsController {
description: 'Maximum number of reports to return',
})
@Get('latest')
async findLatest(@Query(ValidationPipe) queryDto: GetReportsQueryDto): Promise<Report[]> {
return this.reportsService.findLatest(queryDto);
async findLatest(
@Query(ValidationPipe) queryDto: GetReportsQueryDto,
@Req() request: RequestWithUser,
): Promise<Report[]> {
const userId = this.extractUserId(request);
return this.reportsService.findLatest(queryDto, userId);
}

@ApiOperation({ summary: 'GET report' })
@ApiResponse({
status: 200,
description: 'Report status updated successfully',
description: 'Report details',
type: Report,
})
@ApiResponse({
Expand All @@ -60,8 +76,9 @@ export class ReportsController {
description: 'Report ID',
})
@Get(':id')
async getReport(@Param('id') id: string): Promise<Report> {
return this.reportsService.findOne(id);
async getReport(@Param('id') id: string, @Req() request: RequestWithUser): Promise<Report> {
const userId = this.extractUserId(request);
return this.reportsService.findOne(id, userId);
}

@ApiOperation({ summary: 'Update report status' })
Expand All @@ -82,7 +99,20 @@ export class ReportsController {
async updateStatus(
@Param('id') id: string,
@Body(ValidationPipe) updateDto: UpdateReportStatusDto,
@Req() request: RequestWithUser,
): Promise<Report> {
return this.reportsService.updateStatus(id, updateDto);
const userId = this.extractUserId(request);
return this.reportsService.updateStatus(id, updateDto, userId);
}

private extractUserId(request: RequestWithUser): string {
// The user object is attached to the request by our middleware
const user = request.user;

if (!user || !user.sub) {
throw new UnauthorizedException('User ID not found in request');
}

return user.sub;
}
}
Loading