From bfe9291cf5781cd6cbef1d2846fb5778120c4fea Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Wed, 26 Aug 2026 15:49:16 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20ServiceKeyGuard=20=E2=80=94=20n8n=20aut?= =?UTF-8?q?omation=20via=20X-Service-Key=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/guards/service-key.guard.ts | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/common/guards/service-key.guard.ts 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; + } +}