import { CanActivate, ExecutionContext, Injectable, Logger, UnauthorizedException, } from '@nestjs/common'; import { ApiKeysService } from './api-keys.service'; @Injectable() export class ApiKeyGuard implements CanActivate { private readonly logger = new Logger(ApiKeyGuard.name); constructor(private readonly apiKeysService: ApiKeysService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const method: string = request.method; const url: string = request.url; // Check header (X-API-Key) let apiKey = request.headers['x-api-key'] || request.headers['X-API-Key']; let source = 'X-API-Key header'; // Fallback to query parameter (apiKey) if (!apiKey) { apiKey = request.query['apiKey']; if (apiKey) source = 'apiKey query param'; } // Fallback to Authorization: Bearer (used by SSE clients that can't set X-API-Key) if (!apiKey) { const auth: string | undefined = request.headers['authorization']; if (auth?.startsWith('Bearer ')) { apiKey = auth.slice(7); source = 'Authorization: Bearer'; } } this.logger.log( `[${method} ${url}] key source: ${apiKey ? source : 'NONE'} | ` + `headers: ${JSON.stringify(Object.keys(request.headers))} | ` + `key prefix: ${apiKey ? String(apiKey).slice(0, 8) + '…' : 'n/a'}`, ); if (!apiKey) { this.logger.warn(`[${method} ${url}] rejected – no API key found`); throw new UnauthorizedException('API Key missing'); } try { const keyEntry = await this.apiKeysService.validateKey(apiKey as string); this.logger.log( `[${method} ${url}] accepted – key "${keyEntry.name}" (id=${keyEntry.id})`, ); request.apiKeyMetadata = { id: keyEntry.id, name: keyEntry.name }; return true; } catch (err) { this.logger.warn( `[${method} ${url}] rejected – validation failed: ${err.message}`, ); throw new UnauthorizedException(err.message || 'Invalid API Key'); } } }