feat: enhance CORS configuration and implement throttling for API endpoints; add admin role checks in controllers
This commit is contained in:
@@ -23,6 +23,8 @@ MISTRAL_API_KEY=
|
||||
# Publik URL (används av frontend)
|
||||
NEXT_PUBLIC_APP_URL=https://recept.gynther.se
|
||||
NEXT_PUBLIC_API_URL=https://recept.gynther.se
|
||||
# CORS — tillåtna origins för backend-API (normalt samma som APP_URL)
|
||||
ALLOWED_ORIGIN=https://recept.gynther.se
|
||||
|
||||
# Bootstrap-användare (skapas/uppdateras vid appstart)
|
||||
ADMIN_NADMIN_PASSWORD=byt-ut-mig
|
||||
|
||||
@@ -10,23 +10,11 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
const path = request.path;
|
||||
const method = request.method;
|
||||
console.log(`[JwtAuthGuard.canActivate] ${method} ${path}`);
|
||||
console.log(`[JwtAuthGuard.canActivate] Authorization header:`, authHeader ? 'YES' : 'NO');
|
||||
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
console.log(`[JwtAuthGuard.canActivate] isPublic:`, isPublic);
|
||||
|
||||
if (isPublic) return true;
|
||||
|
||||
const result = super.canActivate(context);
|
||||
console.log(`[JwtAuthGuard.canActivate] super.canActivate result:`, result);
|
||||
return result;
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
}
|
||||
|
||||
async validate(payload: { sub: number; username: string; role: string; isPremium: boolean }) {
|
||||
console.log('[JwtStrategy.validate] Payload received:', payload);
|
||||
const result = { userId: payload.sub, username: payload.username, role: payload.role ?? 'user', isPremium: payload.isPremium ?? false };
|
||||
console.log('[JwtStrategy.validate] Returning user:', result);
|
||||
return result;
|
||||
return { userId: payload.sub, username: payload.username, role: payload.role ?? 'user', isPremium: payload.isPremium ?? false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ import helmet from 'helmet';
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// CORS — tillåt endast appens egen origin (sätts via ALLOWED_ORIGIN i miljövariabler)
|
||||
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'https://recept.gynther.se';
|
||||
app.enableCors({
|
||||
origin: allowedOrigin,
|
||||
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Helmet som säkerhetsbackup (CSP hanteras av Next.js/Caddy)
|
||||
app.use(
|
||||
helmet({
|
||||
|
||||
@@ -63,11 +63,13 @@ export class ProductsController {
|
||||
return this.productsService.findAllTags();
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Get('duplicates')
|
||||
findDuplicates() {
|
||||
return this.productsService.findDuplicateCandidates();
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Get('merge-preview')
|
||||
previewMerge(
|
||||
@Query('sourceProductId', ParseIntPipe) sourceProductId: number,
|
||||
@@ -130,10 +132,7 @@ export class ProductsController {
|
||||
|
||||
@Roles('admin')
|
||||
@Post()
|
||||
create(@Body() body: CreateProductDto, @Request() req: any) {
|
||||
console.log('[ProductsController.create] Request received');
|
||||
console.log('[ProductsController.create] User:', req.user);
|
||||
console.log('[ProductsController.create] Body:', body);
|
||||
create(@Body() body: CreateProductDto) {
|
||||
return this.productsService.create(body);
|
||||
}
|
||||
|
||||
@@ -151,6 +150,7 @@ export class ProductsController {
|
||||
return this.productsService.merge(body.sourceProductId, body.targetProductId);
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Patch(':id/canonical-name')
|
||||
updateCanonicalName(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@@ -159,6 +159,7 @@ export class ProductsController {
|
||||
return this.productsService.updateCanonicalName(id, body.canonicalName);
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Put(':id/tags')
|
||||
setTags(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@@ -167,6 +168,7 @@ export class ProductsController {
|
||||
return this.productsService.setTags(id, body.tags);
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Put(':id/nutrition')
|
||||
upsertNutrition(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@@ -175,6 +177,7 @@ export class ProductsController {
|
||||
return this.productsService.upsertNutrition(id, body);
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { QuickImportDto } from './dto/quick-import.dto';
|
||||
@@ -9,6 +10,7 @@ export class QuickImportController {
|
||||
constructor(private readonly quickImportService: QuickImportService) {}
|
||||
|
||||
@Post()
|
||||
@Throttle({ default: { ttl: 60_000, limit: 20 } })
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
|
||||
import { ReceiptAliasService } from './receipt-alias.service';
|
||||
import { CreateReceiptAliasDto } from './dto/create-receipt-alias.dto';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
|
||||
@Roles('admin')
|
||||
@Controller('receipt-aliases')
|
||||
export class ReceiptAliasController {
|
||||
constructor(private readonly receiptAliasService: ReceiptAliasService) {}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Post,
|
||||
Request,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
@@ -12,7 +11,6 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { ReceiptImportService } from './receipt-import.service';
|
||||
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
|
||||
const ALLOWED_MIMES = [
|
||||
'image/jpeg',
|
||||
@@ -29,7 +27,6 @@ export class ReceiptImportController {
|
||||
|
||||
@Post()
|
||||
@Throttle({ default: { ttl: 60_000, limit: 20 } })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
|
||||
@@ -43,6 +43,7 @@ services:
|
||||
DATABASE_URL: "mysql://root:${MARIADB_ROOT_PASSWORD}@recipe-db:3306/${MARIADB_DATABASE}"
|
||||
MISTRAL_API_KEY: "${MISTRAL_API_KEY:-}"
|
||||
JWT_SECRET: "${JWT_SECRET}"
|
||||
ALLOWED_ORIGIN: "${NEXT_PUBLIC_APP_URL}"
|
||||
ADMIN_NADMIN_PASSWORD: "${ADMIN_NADMIN_PASSWORD}"
|
||||
ADMIN_PADMIN_PASSWORD: "${ADMIN_PADMIN_PASSWORD}"
|
||||
SEED_USER1_PASSWORD: "${SEED_USER1_PASSWORD}"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { auth } from './auth';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export default auth((req) => {
|
||||
const { pathname } = req.nextUrl;
|
||||
|
||||
if (pathname.startsWith('/admin')) {
|
||||
const role = (req.auth?.user as any)?.role;
|
||||
if (role !== 'admin') {
|
||||
const loginUrl = new URL('/login', req.url);
|
||||
loginUrl.searchParams.set('callbackUrl', pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: ['/admin/:path*'],
|
||||
};
|
||||
Reference in New Issue
Block a user