security: add helmet, rate limiting, webhook auth, fix token storage, restrict hard deletes
- Add helmet for HTTP security headers (CSP, HSTS, X-Frame-Options, etc.) - Add @nestjs/throttler for rate limiting (100 req/60s per IP) - Add shared secret validation on Signal webhook endpoint - Remove JWT token from localStorage, use Auth0 SDK memory cache with async getAccessTokenSilently() in API interceptor - Restrict hard delete (?hard=true) to ADMINISTRATOR role in service layer - Replace exposed Anthropic API key with placeholder in .env Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
@@ -26,6 +27,12 @@ import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
|
||||
// Rate limiting: 100 requests per 60 seconds per IP
|
||||
ThrottlerModule.forRoot([{
|
||||
ttl: 60000,
|
||||
limit: 100,
|
||||
}]),
|
||||
|
||||
// Core modules
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
@@ -51,6 +58,11 @@ import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
// Apply rate limiting globally
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -220,8 +220,9 @@ export class DriversController {
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
@CurrentUser() user?: any,
|
||||
) {
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.driversService.remove(id, isHardDelete);
|
||||
return this.driversService.remove(id, isHardDelete, user?.role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateDriverDto, UpdateDriverDto } from './dto';
|
||||
|
||||
@@ -78,7 +78,11 @@ export class DriversService {
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
async remove(id: string, hardDelete = false, userRole?: string) {
|
||||
if (hardDelete && userRole !== 'ADMINISTRATOR') {
|
||||
throw new ForbiddenException('Only administrators can permanently delete records');
|
||||
}
|
||||
|
||||
const driver = await this.findOne(id);
|
||||
|
||||
if (hardDelete) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { EventsService } from './events.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CreateEventDto, UpdateEventDto, UpdateEventStatusDto } from './dto';
|
||||
|
||||
@@ -59,8 +60,9 @@ export class EventsController {
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
@CurrentUser() user?: any,
|
||||
) {
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.eventsService.remove(id, isHardDelete);
|
||||
return this.eventsService.remove(id, isHardDelete, user?.role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
@@ -248,7 +249,11 @@ export class EventsService {
|
||||
return this.enrichEventWithVips(updatedEvent);
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
async remove(id: string, hardDelete = false, userRole?: string) {
|
||||
if (hardDelete && userRole !== 'ADMINISTRATOR') {
|
||||
throw new ForbiddenException('Only administrators can permanently delete records');
|
||||
}
|
||||
|
||||
const event = await this.findOne(id);
|
||||
|
||||
if (hardDelete) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { json, urlencoded } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { AppModule } from './app.module';
|
||||
import { AllExceptionsFilter, HttpExceptionFilter } from './common/filters';
|
||||
|
||||
@@ -9,6 +10,9 @@ async function bootstrap() {
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Security headers
|
||||
app.use(helmet());
|
||||
|
||||
// Increase body size limit for PDF attachments (base64 encoded)
|
||||
app.use(json({ limit: '5mb' }));
|
||||
app.use(urlencoded({ extended: true, limit: '5mb' }));
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
Logger,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { Request, Response } from 'express';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
@@ -105,7 +106,14 @@ export class MessagesController {
|
||||
*/
|
||||
@Public()
|
||||
@Post('webhook')
|
||||
async handleWebhook(@Body() payload: SignalWebhookPayload) {
|
||||
async handleWebhook(@Body() payload: SignalWebhookPayload, @Req() req: Request) {
|
||||
// Validate webhook secret if configured
|
||||
const secret = process.env.SIGNAL_WEBHOOK_SECRET;
|
||||
if (secret && req.headers['x-webhook-secret'] !== secret) {
|
||||
this.logger.warn('Webhook rejected: invalid or missing secret');
|
||||
return { success: false, error: 'Unauthorized' };
|
||||
}
|
||||
|
||||
this.logger.debug('Received Signal webhook:', JSON.stringify(payload));
|
||||
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { VehiclesService } from './vehicles.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CreateVehicleDto, UpdateVehicleDto } from './dto';
|
||||
|
||||
@@ -56,8 +57,12 @@ export class VehiclesController {
|
||||
|
||||
@Delete(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
remove(@Param('id') id: string, @Query('hard') hard?: string) {
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
@CurrentUser() user?: any,
|
||||
) {
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.vehiclesService.remove(id, isHardDelete);
|
||||
return this.vehiclesService.remove(id, isHardDelete, user?.role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateVehicleDto, UpdateVehicleDto } from './dto';
|
||||
|
||||
@@ -89,7 +89,11 @@ export class VehiclesService {
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
async remove(id: string, hardDelete = false, userRole?: string) {
|
||||
if (hardDelete && userRole !== 'ADMINISTRATOR') {
|
||||
throw new ForbiddenException('Only administrators can permanently delete records');
|
||||
}
|
||||
|
||||
const vehicle = await this.findOne(id);
|
||||
|
||||
if (hardDelete) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { VipsService } from './vips.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { AbilitiesGuard } from '../auth/guards/abilities.guard';
|
||||
import { CanCreate, CanRead, CanUpdate, CanDelete } from '../auth/decorators/check-ability.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { CreateVipDto, UpdateVipDto } from './dto';
|
||||
|
||||
@Controller('vips')
|
||||
@@ -49,9 +50,10 @@ export class VipsController {
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
@CurrentUser() user?: any,
|
||||
) {
|
||||
// Only administrators can hard delete
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.vipsService.remove(id, isHardDelete);
|
||||
return this.vipsService.remove(id, isHardDelete, user?.role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateVipDto, UpdateVipDto } from './dto';
|
||||
|
||||
@@ -58,7 +58,11 @@ export class VipsService {
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
async remove(id: string, hardDelete = false, userRole?: string) {
|
||||
if (hardDelete && userRole !== 'ADMINISTRATOR') {
|
||||
throw new ForbiddenException('Only administrators can permanently delete records');
|
||||
}
|
||||
|
||||
const vip = await this.findOne(id);
|
||||
|
||||
if (hardDelete) {
|
||||
|
||||
Reference in New Issue
Block a user