diff --git a/src/common/guards/service-key.guard.ts b/src/common/guards/service-key.guard.ts new file mode 100644 index 0000000..c460a95 --- /dev/null +++ b/src/common/guards/service-key.guard.ts @@ -0,0 +1,49 @@ +import { + CanActivate, ExecutionContext, Injectable, UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Reflector } from '@nestjs/core'; +import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; + +/** + * ServiceKeyGuard — accepts X-Service-Key header for automation tools (n8n, cron scripts). + * Injects tenantId from N8N_DEFAULT_TENANT_ID env var. + * Only activates when a valid key is present; falls through to JWT guard otherwise. + */ +@Injectable() +export class ServiceKeyGuard implements CanActivate { + private readonly serviceKey: string; + private readonly defaultTenantId: string; + + constructor( + private readonly config: ConfigService, + private readonly reflector: Reflector, + ) { + this.serviceKey = config.get('N8N_SERVICE_KEY', ''); + this.defaultTenantId = config.get('N8N_DEFAULT_TENANT_ID', ''); + } + + canActivate(ctx: ExecutionContext): boolean { + // Skip on @Public() routes + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + ctx.getHandler(), + ctx.getClass(), + ]); + if (isPublic) return true; + + if (!this.serviceKey) return false; // service key not configured → fall through + + const req = ctx.switchToHttp().getRequest(); + const provided = req.headers['x-service-key'] ?? ''; + + if (!provided || provided !== this.serviceKey) return false; + + if (!this.defaultTenantId) { + throw new UnauthorizedException('N8N_DEFAULT_TENANT_ID not configured'); + } + + // Inject synthetic session so @CurrentSession() works + req.session = { tenantId: this.defaultTenantId, userId: 'n8n-service', isService: true }; + return true; + } +}