fix(gateway): security hardening — auth guards, ownership checks, validation, rate limiting

This commit is contained in:
2026-03-13 08:25:57 -05:00
parent 01e9891243
commit 55b5a31c3c
22 changed files with 696 additions and 74 deletions

View File

@@ -16,7 +16,8 @@ import type { Brain } from '@mosaic/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import type {
import { assertOwner } from '../auth/resource-ownership.js';
import {
CreateConversationDto,
UpdateConversationDto,
SendMessageDto,
@@ -33,10 +34,8 @@ export class ConversationsController {
}
@Get(':id')
async findOne(@Param('id') id: string) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
return conversation;
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
return this.getOwnedConversation(id, user.id);
}
@Post()
@@ -49,7 +48,12 @@ export class ConversationsController {
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateConversationDto) {
async update(
@Param('id') id: string,
@Body() dto: UpdateConversationDto,
@CurrentUser() user: { id: string },
) {
await this.getOwnedConversation(id, user.id);
const conversation = await this.brain.conversations.update(id, dto);
if (!conversation) throw new NotFoundException('Conversation not found');
return conversation;
@@ -57,22 +61,25 @@ export class ConversationsController {
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
await this.getOwnedConversation(id, user.id);
const deleted = await this.brain.conversations.remove(id);
if (!deleted) throw new NotFoundException('Conversation not found');
}
@Get(':id/messages')
async listMessages(@Param('id') id: string) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
async listMessages(@Param('id') id: string, @CurrentUser() user: { id: string }) {
await this.getOwnedConversation(id, user.id);
return this.brain.conversations.findMessages(id);
}
@Post(':id/messages')
async addMessage(@Param('id') id: string, @Body() dto: SendMessageDto) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
async addMessage(
@Param('id') id: string,
@Body() dto: SendMessageDto,
@CurrentUser() user: { id: string },
) {
await this.getOwnedConversation(id, user.id);
return this.brain.conversations.addMessage({
conversationId: id,
role: dto.role,
@@ -80,4 +87,11 @@ export class ConversationsController {
metadata: dto.metadata,
});
}
private async getOwnedConversation(id: string, userId: string) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
assertOwner(conversation.userId, userId, 'Conversation');
return conversation;
}
}

View File

@@ -1,15 +1,36 @@
export interface CreateConversationDto {
import { IsIn, IsObject, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
export class CreateConversationDto {
@IsOptional()
@IsString()
@MaxLength(255)
title?: string;
@IsOptional()
@IsUUID()
projectId?: string;
}
export interface UpdateConversationDto {
export class UpdateConversationDto {
@IsOptional()
@IsString()
@MaxLength(255)
title?: string;
@IsOptional()
@IsUUID()
projectId?: string | null;
}
export interface SendMessageDto {
role: 'user' | 'assistant' | 'system';
content: string;
export class SendMessageDto {
@IsIn(['user', 'assistant', 'system'])
role!: 'user' | 'assistant' | 'system';
@IsString()
@MaxLength(10_000)
content!: string;
@IsOptional()
@IsObject()
metadata?: Record<string, unknown>;
}