From 9bca8bd2fcf559ec000093efa90c905d623642bc Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sun, 15 Feb 2026 22:52:25 -0500 Subject: [PATCH] fix: Response wrapping, error logging, and controller hardening (COA 3) - HttpExceptionFilter: Log actual error details for non-HttpExceptions (was silently swallowing 500s) - ServersService: Return null fields instead of 404 for new licenses without servers - NotificationsController: Wrap config responses as { config } to match frontend expectations - WebstoreController: Wrap config responses as { config } to match frontend expectations - ChatController: Replace ParseIntPipe with manual parseInt (400 on missing optional param) - WipesController: Same ParseIntPipe fix for history limit param Co-Authored-By: Claude Opus 4.6 --- .../src/common/filters/http-exception.filter.ts | 11 +++++++++++ backend-nest/src/modules/chat/chat.controller.ts | 7 ++++--- .../notifications/notifications.controller.ts | 6 ++++-- backend-nest/src/modules/servers/servers.service.ts | 13 +++++++------ .../src/modules/webstore/webstore.controller.ts | 6 ++++-- backend-nest/src/modules/wipes/wipes.controller.ts | 6 +++--- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/backend-nest/src/common/filters/http-exception.filter.ts b/backend-nest/src/common/filters/http-exception.filter.ts index 783d476..039f1f2 100644 --- a/backend-nest/src/common/filters/http-exception.filter.ts +++ b/backend-nest/src/common/filters/http-exception.filter.ts @@ -4,14 +4,18 @@ import { ArgumentsHost, HttpException, HttpStatus, + Logger, } from '@nestjs/common'; import { Response } from 'express'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger('ExceptionFilter'); + catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); + const request = ctx.getRequest(); let status = HttpStatus.INTERNAL_SERVER_ERROR; let message = 'Internal server error'; @@ -28,6 +32,13 @@ export class HttpExceptionFilter implements ExceptionFilter { message = obj.message[0] as string; } } + } else { + // Log non-HttpException errors with full details + const err = exception instanceof Error ? exception : new Error(String(exception)); + this.logger.error( + `Unhandled exception on ${request.method} ${request.url}: ${err.message}`, + err.stack, + ); } response.status(status).json({ message }); diff --git a/backend-nest/src/modules/chat/chat.controller.ts b/backend-nest/src/modules/chat/chat.controller.ts index 14981bc..fd8ecb9 100644 --- a/backend-nest/src/modules/chat/chat.controller.ts +++ b/backend-nest/src/modules/chat/chat.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Put, Param, Body, Query, ParseIntPipe, UseGuards } from '@nestjs/common'; +import { Controller, Get, Put, Param, Body, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; import { ChatService } from './chat.service'; import { FlagMessageDto } from './dto/flag-message.dto'; @@ -21,9 +21,10 @@ export class ChatController { @ApiQuery({ name: 'limit', required: false, example: 100 }) async getMessages( @CurrentTenant() licenseId: string, - @Query('limit', new ParseIntPipe({ optional: true })) limit?: number, + @Query('limit') limit?: string, ) { - return await this.chatService.getMessages(licenseId, limit || 100); + const limitNum = limit ? parseInt(limit, 10) : 100; + return await this.chatService.getMessages(licenseId, limitNum); } @Put(':id/flag') diff --git a/backend-nest/src/modules/notifications/notifications.controller.ts b/backend-nest/src/modules/notifications/notifications.controller.ts index 5b0bf8d..704fc2b 100644 --- a/backend-nest/src/modules/notifications/notifications.controller.ts +++ b/backend-nest/src/modules/notifications/notifications.controller.ts @@ -27,7 +27,8 @@ export class NotificationsController { description: 'Notification config retrieved successfully', }) async getConfig(@CurrentTenant() licenseId: string) { - return await this.notificationsService.getConfig(licenseId); + const config = await this.notificationsService.getConfig(licenseId); + return { config }; } @Put('config') @@ -43,6 +44,7 @@ export class NotificationsController { @CurrentTenant() licenseId: string, @Body() dto: UpdateConfigDto, ) { - return await this.notificationsService.updateConfig(licenseId, dto); + const config = await this.notificationsService.updateConfig(licenseId, dto); + return { config }; } } diff --git a/backend-nest/src/modules/servers/servers.service.ts b/backend-nest/src/modules/servers/servers.service.ts index d850eda..a5afef4 100644 --- a/backend-nest/src/modules/servers/servers.service.ts +++ b/backend-nest/src/modules/servers/servers.service.ts @@ -17,7 +17,8 @@ export class ServersService { ) {} /** - * Get server connection and config for a license + * Get server connection and config for a license. + * Returns null fields if no server has been set up yet. */ async getServer(licenseId: string) { const connection = await this.connectionRepo.findOne({ @@ -28,11 +29,11 @@ export class ServersService { where: { license_id: licenseId }, }); - if (!connection || !config) { - throw new NotFoundException('Server not found for this license'); - } - - return { connection, config }; + return { + connection: connection || null, + config: config || null, + setup_required: !connection || !config, + }; } /** diff --git a/backend-nest/src/modules/webstore/webstore.controller.ts b/backend-nest/src/modules/webstore/webstore.controller.ts index 2f88a78..a589b6d 100644 --- a/backend-nest/src/modules/webstore/webstore.controller.ts +++ b/backend-nest/src/modules/webstore/webstore.controller.ts @@ -18,7 +18,8 @@ export class WebstoreController { @ApiBearerAuth() @ApiOperation({ summary: 'Get webstore configuration' }) async getConfig(@CurrentTenant() licenseId: string) { - return this.webstoreService.getConfig(licenseId); + const config = await this.webstoreService.getConfig(licenseId); + return { config }; } @Put('webstore/config') @@ -28,7 +29,8 @@ export class WebstoreController { @CurrentTenant() licenseId: string, @Body() dto: UpdateStoreConfigDto, ) { - return this.webstoreService.updateConfig(licenseId, dto); + const config = await this.webstoreService.updateConfig(licenseId, dto); + return { config }; } @Get('webstore/categories') diff --git a/backend-nest/src/modules/wipes/wipes.controller.ts b/backend-nest/src/modules/wipes/wipes.controller.ts index c887fd1..69b51ef 100644 --- a/backend-nest/src/modules/wipes/wipes.controller.ts +++ b/backend-nest/src/modules/wipes/wipes.controller.ts @@ -7,7 +7,6 @@ import { Body, Param, Query, - ParseIntPipe, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; @@ -81,9 +80,10 @@ export class WipesController { @ApiQuery({ name: 'limit', required: false, example: 50 }) getHistory( @CurrentTenant() licenseId: string, - @Query('limit', new ParseIntPipe({ optional: true })) limit?: number, + @Query('limit') limit?: string, ) { - return this.wipesService.getHistory(licenseId, limit || 50); + const limitNum = limit ? parseInt(limit, 10) : 50; + return this.wipesService.getHistory(licenseId, limitNum); } @Post('trigger')