diff --git a/.env.example b/.env.example index 475dc96..6a0dd56 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ NODE_ENV=development PORT=3001 +# Comma-separated list of allowed browser origins for CORS -- never use a wildcard in production +CORS_ORIGINS=http://localhost:3000 + # Supabase (CEO-OS project, Coolify) DATABASE_URL= SUPABASE_URL= diff --git a/src/app.module.ts b/src/app.module.ts index 74deeba..181c180 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,12 +1,14 @@ import { Module } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; import { ConfigModule } from '@nestjs/config'; -import { ThrottlerModule } from '@nestjs/throttler'; +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { ScheduleModule } from '@nestjs/schedule'; import { BullModule } from '@nestjs/bullmq'; import { LoggerModule } from 'nestjs-pino'; import { HealthController } from './health/health.controller'; import { AuthModule } from './auth/auth.module'; import { EventsModule } from './events/events.module'; +import { TenantGuard } from './auth/tenant.guard'; function parseRedisConnection(redisUrl: string | undefined) { if (!redisUrl) { @@ -31,5 +33,9 @@ function parseRedisConnection(redisUrl: string | undefined) { EventsModule, ], controllers: [HealthController], + providers: [ + { provide: APP_GUARD, useClass: ThrottlerGuard }, + { provide: APP_GUARD, useClass: TenantGuard }, + ], }) export class AppModule {} diff --git a/src/auth/public.decorator.ts b/src/auth/public.decorator.ts new file mode 100644 index 0000000..0478a07 --- /dev/null +++ b/src/auth/public.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; + +// Marchez explicit rutele fara sesiune (health checks, webhooks semnate separat). +// Fara acest decorator, TenantGuard respinge implicit orice request -- deny-by-default, +// per blueprint 11.3, in loc sa se bazeze pe fiecare controller nou sa adauge garda manual. +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/auth/tenant.guard.ts b/src/auth/tenant.guard.ts index 48cee70..22bc1aa 100644 --- a/src/auth/tenant.guard.ts +++ b/src/auth/tenant.guard.ts @@ -1,5 +1,7 @@ import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; import type { Request } from 'express'; +import { IS_PUBLIC_KEY } from './public.decorator'; export interface AuthenticatedSession { userId: string; @@ -15,10 +17,22 @@ interface RequestWithSession extends Request { * Blueprint 11.3: tenant_id vine intotdeauna din sesiune, niciodata din * body/query/params. Orice request fara sesiune valida, sau care incearca * sa suprascrie tenant_id din client, este respins (deny-by-default). + * Inregistrata global (APP_GUARD) -- rutele publice trebuie sa foloseasca + * explicit @Public(), altfel raman blocate implicit. */ @Injectable() export class TenantGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + canActivate(context: ExecutionContext): boolean { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + const request = context.switchToHttp().getRequest(); const session = request.session; diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts index 0c9989a..0ed8bd9 100644 --- a/src/health/health.controller.ts +++ b/src/health/health.controller.ts @@ -1,7 +1,9 @@ import { Controller, Get } from '@nestjs/common'; +import { Public } from '../auth/public.decorator'; @Controller('health') export class HealthController { + @Public() @Get() check() { return { status: 'ok', service: 'ceo-api', timestamp: new Date().toISOString() }; diff --git a/src/main.ts b/src/main.ts index 89410b5..847bb57 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,8 @@ async function bootstrap() { const app = await NestFactory.create(AppModule, { bufferLogs: true }); app.useLogger(app.get(Logger)); app.use(helmet()); - app.enableCors(); + const allowedOrigins = (process.env.CORS_ORIGINS ?? 'http://localhost:3000').split(',').map((o) => o.trim()); + app.enableCors({ origin: allowedOrigins, credentials: true }); const config = new DocumentBuilder() .setTitle('CEO OS API')