feat: implement security headers and rate limiting; update environment variables and documentation

This commit is contained in:
Nils-Johan Gynther
2026-04-21 08:06:21 +02:00
parent c1d51c771e
commit 7748ad311f
13 changed files with 133 additions and 23 deletions
+12
View File
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { HealthModule } from './health/health.module';
import { PrismaModule } from './prisma/prisma.module';
import { ProductsModule } from './products/products.module';
@@ -21,6 +22,13 @@ import { RolesGuard } from './auth/roles.guard';
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'default',
ttl: 60_000, // 1 minut
limit: 120, // 120 anrop per minut (generellt)
},
]),
HealthModule,
PrismaModule,
ProductsModule,
@@ -38,6 +46,10 @@ import { RolesGuard } from './auth/roles.guard';
AiModule,
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
+3
View File
@@ -1,4 +1,5 @@
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@@ -9,12 +10,14 @@ export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Throttle({ default: { ttl: 60_000, limit: 10 } })
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Public()
@Throttle({ default: { ttl: 60_000, limit: 10 } })
@HttpCode(HttpStatus.OK)
@Post('login')
login(@Body() dto: LoginDto) {
+5 -1
View File
@@ -11,7 +11,11 @@ import { UsersModule } from '../users/users.module';
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET ?? 'changeme',
secret: (() => {
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET saknas i miljövariabler');
return secret;
})(),
signOptions: { expiresIn: '7d' },
}),
],
+3 -1
View File
@@ -5,10 +5,12 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET saknas i miljövariabler');
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET ?? 'changeme',
secretOrKey: secret,
});
}
@@ -13,6 +13,7 @@ import {
Query,
Request,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { Public } from '../auth/decorators/public.decorator';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@@ -89,6 +90,7 @@ export class ProductsController {
@Roles('admin')
@Post('ai-categorize-bulk')
@Throttle({ default: { ttl: 60_000, limit: 5 } })
@HttpCode(200)
async aiCategorizeBulk(@Body() body: AiCategorizeBulkDto) {
const categories = await this.categoriesService.findFlattened();
@@ -113,6 +115,7 @@ export class ProductsController {
}
@Get(':id/suggest-category')
@Throttle({ default: { ttl: 60_000, limit: 20 } })
async suggestCategory(
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { role: string; isPremium: boolean } },
@@ -7,6 +7,7 @@ import {
UseInterceptors,
BadRequestException,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { ReceiptImportService } from './receipt-import.service';
@@ -27,6 +28,7 @@ export class ReceiptImportController {
constructor(private readonly receiptImportService: ReceiptImportService) {}
@Post()
@Throttle({ default: { ttl: 60_000, limit: 20 } })
@UseGuards(JwtAuthGuard)
@UseInterceptors(
FileInterceptor('file', {