CRITICAL SECURITY FIX: Require authentication for AI chat endpoints
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- Remove @Public decorators from AI conversation endpoints
- Add proper authentication checks for all AI endpoints
- Prevent users from seeing conversations from other users/families
- Add UnauthorizedException for unauthenticated requests
- Fix privacy leak where all users could see all conversations

This was a critical security vulnerability that allowed any user to access
conversations from other users and families. All AI endpoints now properly
require authentication and filter data by the authenticated user's ID.
This commit is contained in:
2025-10-05 18:59:20 +00:00
parent 8e760d8323
commit 5c255298d4

View File

@@ -9,6 +9,7 @@ import {
Req, Req,
Res, Res,
Header, Header,
UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
import { AIService } from './ai.service'; import { AIService } from './ai.service';
@@ -20,10 +21,12 @@ import { Public } from '../auth/decorators/public.decorator';
export class AIController { export class AIController {
constructor(private readonly aiService: AIService) {} constructor(private readonly aiService: AIService) {}
@Public() // Public for testing
@Post('chat') @Post('chat')
async chat(@Req() req: any, @Body() chatDto: ChatMessageDto) { async chat(@Req() req: any, @Body() chatDto: ChatMessageDto) {
const userId = req.user?.userId || 'test_user_123'; // Use test user if not authenticated const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
const response = await this.aiService.chat(userId, chatDto); const response = await this.aiService.chat(userId, chatDto);
return { return {
success: true, success: true,
@@ -35,7 +38,6 @@ export class AIController {
* Streaming chat endpoint * Streaming chat endpoint
* Returns Server-Sent Events (SSE) for real-time streaming responses * Returns Server-Sent Events (SSE) for real-time streaming responses
*/ */
@Public() // Public for testing
@Post('chat/stream') @Post('chat/stream')
@Header('Content-Type', 'text/event-stream') @Header('Content-Type', 'text/event-stream')
@Header('Cache-Control', 'no-cache') @Header('Cache-Control', 'no-cache')
@@ -45,7 +47,11 @@ export class AIController {
@Body() chatDto: ChatMessageDto, @Body() chatDto: ChatMessageDto,
@Res() res: Response, @Res() res: Response,
) { ) {
const userId = req.user?.userId || 'test_user_123'; const userId = req.user?.userId;
if (!userId) {
res.status(401).json({ error: 'Authentication required' });
return;
}
// Set up SSE headers // Set up SSE headers
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
@@ -72,10 +78,12 @@ export class AIController {
} }
} }
@Public() // Public for testing
@Get('conversations') @Get('conversations')
async getConversations(@Req() req: any) { async getConversations(@Req() req: any) {
const userId = req.user?.userId || 'test_user_123'; const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
const conversations = await this.aiService.getUserConversations(userId); const conversations = await this.aiService.getUserConversations(userId);
return { return {
success: true, success: true,
@@ -83,10 +91,12 @@ export class AIController {
}; };
} }
@Public() // Public for testing
@Get('conversations/:id') @Get('conversations/:id')
async getConversation(@Req() req: any, @Param('id') conversationId: string) { async getConversation(@Req() req: any, @Param('id') conversationId: string) {
const userId = req.user?.userId || 'test_user_123'; const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
const conversation = await this.aiService.getConversation( const conversation = await this.aiService.getConversation(
userId, userId,
conversationId, conversationId,
@@ -109,14 +119,16 @@ export class AIController {
}; };
} }
@Public() // Public for testing
@Patch('conversations/:id/group') @Patch('conversations/:id/group')
async updateConversationGroup( async updateConversationGroup(
@Req() req: any, @Req() req: any,
@Param('id') conversationId: string, @Param('id') conversationId: string,
@Body() body: { groupName: string | null }, @Body() body: { groupName: string | null },
) { ) {
const userId = req.user?.userId || 'test_user_123'; const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
const conversation = await this.aiService.updateConversationGroup( const conversation = await this.aiService.updateConversationGroup(
userId, userId,
conversationId, conversationId,
@@ -128,10 +140,12 @@ export class AIController {
}; };
} }
@Public() // Public for testing
@Get('groups') @Get('groups')
async getConversationGroups(@Req() req: any) { async getConversationGroups(@Req() req: any) {
const userId = req.user?.userId || 'test_user_123'; const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
const groups = await this.aiService.getConversationGroups(userId); const groups = await this.aiService.getConversationGroups(userId);
return { return {
success: true, success: true,
@@ -149,10 +163,12 @@ export class AIController {
}; };
} }
@Public() // Public for testing
@Post('feedback') @Post('feedback')
async submitFeedback(@Req() req: any, @Body() feedbackDto: FeedbackDto) { async submitFeedback(@Req() req: any, @Body() feedbackDto: FeedbackDto) {
const userId = req.user?.userId || 'test_user_123'; const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
await this.aiService.submitFeedback( await this.aiService.submitFeedback(
userId, userId,
feedbackDto.conversationId, feedbackDto.conversationId,