From 49328bc4c0519aef39f5529591c2e179ba4489ce Mon Sep 17 00:00:00 2001 From: rmyndharis Date: Wed, 20 May 2026 15:40:34 +0700 Subject: [PATCH] fix(swagger): apply X-API-Key security scheme globally The Swagger document defined the X-API-Key scheme via addApiKey() but never applied it, so no operation declared a security requirement and Swagger UI never sent the key. Requests reached the global ApiKeyGuard with no key and got 401 Unauthorized. Extract the config into createSwaggerConfig(), apply the scheme globally with addSecurityRequirements, and remove 5 stray @ApiBearerAuth() decorators that referenced an undefined bearer scheme. Fixes #104 --- src/config/swagger.config.spec.ts | 12 +++++++++ src/config/swagger.config.ts | 32 +++++++++++++++++++++++ src/main.ts | 18 +++---------- src/modules/auth/auth.controller.ts | 3 +-- src/modules/catalog/catalog.controller.ts | 3 +-- src/modules/plugins/plugins.controller.ts | 3 +-- src/modules/stats/stats.controller.ts | 3 +-- src/modules/status/status.controller.ts | 3 +-- 8 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 src/config/swagger.config.spec.ts create mode 100644 src/config/swagger.config.ts diff --git a/src/config/swagger.config.spec.ts b/src/config/swagger.config.spec.ts new file mode 100644 index 0000000..b5e5324 --- /dev/null +++ b/src/config/swagger.config.spec.ts @@ -0,0 +1,12 @@ +import { createSwaggerConfig } from './swagger.config'; + +describe('createSwaggerConfig', () => { + // Regression test for issue #104: Swagger UI returned "Unauthorized" because the + // X-API-Key scheme was defined but never applied — no operation declared a security + // requirement, so Swagger UI never sent the key. The fix applies it globally. + it('applies the X-API-Key security scheme as a global requirement', () => { + const config = createSwaggerConfig(); + + expect(config.security).toContainEqual({ 'X-API-Key': [] }); + }); +}); diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 0000000..db379f6 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,32 @@ +import { DocumentBuilder, OpenAPIObject } from '@nestjs/swagger'; + +/** + * Security scheme name for the API key, used both when defining the scheme and + * when applying it as a global requirement so Swagger UI sends the header. + */ +export const API_KEY_SECURITY_SCHEME = 'X-API-Key'; + +/** + * Builds the OpenAPI document configuration for the OpenWA API. + */ +export function createSwaggerConfig(): Omit { + return ( + new DocumentBuilder() + .setTitle('OpenWA API') + .setDescription('Open Source WhatsApp API Gateway - Free, Self-Hosted HTTP API') + .setVersion('0.1.6') + .addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }, API_KEY_SECURITY_SCHEME) + // Apply the scheme globally so Swagger UI sends the key with every request + // (mirrors the global ApiKeyGuard). Without this, "Authorize" is cosmetic. + .addSecurityRequirements(API_KEY_SECURITY_SCHEME) + .addTag('sessions', 'WhatsApp session management') + .addTag('messages', 'Send and manage messages') + .addTag('webhooks', 'Webhook configuration') + .addTag('contacts', 'Contact management') + .addTag('groups', 'Group management') + .addTag('labels', 'Label management (WhatsApp Business)') + .addTag('channels', 'Channel/Newsletter management') + .addTag('health', 'Health check endpoints') + .build() + ); +} diff --git a/src/main.ts b/src/main.ts index 61966fb..d598990 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { SwaggerModule } from '@nestjs/swagger'; import helmet from 'helmet'; import { AppModule } from './app.module'; import { ShutdownService } from './common/services/shutdown.service'; +import { createSwaggerConfig } from './config/swagger.config'; import * as dotenv from 'dotenv'; import * as fs from 'fs'; import * as path from 'path'; @@ -141,20 +142,7 @@ async function bootstrap() { ); // Swagger documentation - const config = new DocumentBuilder() - .setTitle('OpenWA API') - .setDescription('Open Source WhatsApp API Gateway - Free, Self-Hosted HTTP API') - .setVersion('0.1.6') - .addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }, 'X-API-Key') - .addTag('sessions', 'WhatsApp session management') - .addTag('messages', 'Send and manage messages') - .addTag('webhooks', 'Webhook configuration') - .addTag('contacts', 'Contact management') - .addTag('groups', 'Group management') - .addTag('labels', 'Label management (WhatsApp Business)') - .addTag('channels', 'Channel/Newsletter management') - .addTag('health', 'Health check endpoints') - .build(); + const config = createSwaggerConfig(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api/docs', app, document); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 3d723f3..dc15780 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,12 +1,11 @@ import { Controller, Get, Post, Put, Delete, Body, Param, HttpCode, HttpStatus } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { CreateApiKeyDto, UpdateApiKeyDto, ApiKeyResponseDto, ApiKeyCreatedResponseDto } from './dto'; import { RequireRole } from './decorators/auth.decorators'; import { ApiKeyRole } from './entities/api-key.entity'; @ApiTags('auth') -@ApiBearerAuth() @Controller('auth/api-keys') export class AuthController { constructor(private readonly authService: AuthService) {} diff --git a/src/modules/catalog/catalog.controller.ts b/src/modules/catalog/catalog.controller.ts index 5fece45..d2ecf98 100644 --- a/src/modules/catalog/catalog.controller.ts +++ b/src/modules/catalog/catalog.controller.ts @@ -1,10 +1,9 @@ import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { CatalogService } from './catalog.service'; import { SendProductDto, SendCatalogDto, ProductQueryDto } from './dto/send-product.dto'; @ApiTags('Catalog') -@ApiBearerAuth() @Controller('sessions/:sessionId') export class CatalogController { constructor(private readonly catalogService: CatalogService) {} diff --git a/src/modules/plugins/plugins.controller.ts b/src/modules/plugins/plugins.controller.ts index 870bcce..4792fec 100644 --- a/src/modules/plugins/plugins.controller.ts +++ b/src/modules/plugins/plugins.controller.ts @@ -1,10 +1,9 @@ import { Controller, Get, Post, Put, Param, Body, HttpCode, HttpStatus } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { PluginsService } from './plugins.service'; import { PluginDto, PluginConfigDto } from './dto/plugin.dto'; @ApiTags('plugins') -@ApiBearerAuth() @Controller('plugins') export class PluginsController { constructor(private readonly pluginsService: PluginsService) {} diff --git a/src/modules/stats/stats.controller.ts b/src/modules/stats/stats.controller.ts index 803e507..edde544 100644 --- a/src/modules/stats/stats.controller.ts +++ b/src/modules/stats/stats.controller.ts @@ -1,10 +1,9 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { StatsService } from './stats.service'; import { StatsQueryDto } from './dto/stats-query.dto'; @ApiTags('Statistics') -@ApiBearerAuth() @Controller('stats') export class StatsController { constructor(private readonly statsService: StatsService) {} diff --git a/src/modules/status/status.controller.ts b/src/modules/status/status.controller.ts index 34574b1..5840695 100644 --- a/src/modules/status/status.controller.ts +++ b/src/modules/status/status.controller.ts @@ -1,11 +1,10 @@ import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { StatusService } from './status.service'; import { SendTextStatusDto } from './dto/send-text-status.dto'; import { SendImageStatusDto, SendVideoStatusDto } from './dto/send-media-status.dto'; @ApiTags('Status') -@ApiBearerAuth() @Controller('sessions/:sessionId/status') export class StatusController { constructor(private readonly statusService: StatusService) {}