feat(#89): implement COMMAND message type for federation
Implements federated command messages following TDD principles and mirroring the QueryService pattern for consistency. ## Implementation ### Schema Changes - Added commandType and payload fields to FederationMessage model - Supports COMMAND message type (already defined in enum) - Applied schema changes with prisma db push ### Type Definitions - CommandMessage: Request structure with commandType and payload - CommandResponse: Response structure with correlation - CommandMessageDetails: Full message details for API responses ### CommandService - sendCommand(): Send command to remote instance with signature - handleIncomingCommand(): Process incoming commands with verification - processCommandResponse(): Handle command responses - getCommandMessages(): List commands for workspace - getCommandMessage(): Get single command details - Full signature verification and timestamp validation - Error handling and status tracking ### CommandController - POST /api/v1/federation/command - Send command (authenticated) - POST /api/v1/federation/incoming/command - Handle incoming (public) - GET /api/v1/federation/commands - List commands (authenticated) - GET /api/v1/federation/commands/:id - Get command (authenticated) ## Testing - CommandService: 15 tests, 90.21% coverage - CommandController: 8 tests, 100% coverage - All 23 tests passing - Exceeds 85% coverage requirement - Total 47 tests passing (includes command tests) ## Security - RSA signature verification for all incoming commands - Timestamp validation to prevent replay attacks - Connection status validation - Authorization checks on command types ## Quality Checks - TypeScript compilation: PASSED - All tests: 47 PASSED - Code coverage: >85% (90.21% for CommandService, 100% for CommandController) - Linting: PASSED Fixes #89 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
91
apps/api/src/federation/command.controller.ts
Normal file
91
apps/api/src/federation/command.controller.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Command Controller
|
||||
*
|
||||
* API endpoints for federated command messages.
|
||||
*/
|
||||
|
||||
import { Controller, Post, Get, Body, Param, Query, UseGuards, Req, Logger } from "@nestjs/common";
|
||||
import { CommandService } from "./command.service";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { SendCommandDto, IncomingCommandDto } from "./dto/command.dto";
|
||||
import type { AuthenticatedRequest } from "../common/types/user.types";
|
||||
import type { CommandMessageDetails, CommandResponse } from "./types/message.types";
|
||||
import type { FederationMessageStatus } from "@prisma/client";
|
||||
|
||||
@Controller("api/v1/federation")
|
||||
export class CommandController {
|
||||
private readonly logger = new Logger(CommandController.name);
|
||||
|
||||
constructor(private readonly commandService: CommandService) {}
|
||||
|
||||
/**
|
||||
* Send a command to a remote instance
|
||||
* Requires authentication
|
||||
*/
|
||||
@Post("command")
|
||||
@UseGuards(AuthGuard)
|
||||
async sendCommand(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Body() dto: SendCommandDto
|
||||
): Promise<CommandMessageDetails> {
|
||||
if (!req.user?.workspaceId) {
|
||||
throw new Error("Workspace ID not found in request");
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`User ${req.user.id} sending command to connection ${dto.connectionId} in workspace ${req.user.workspaceId}`
|
||||
);
|
||||
|
||||
return this.commandService.sendCommand(
|
||||
req.user.workspaceId,
|
||||
dto.connectionId,
|
||||
dto.commandType,
|
||||
dto.payload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming command from remote instance
|
||||
* Public endpoint - no authentication required (signature-based verification)
|
||||
*/
|
||||
@Post("incoming/command")
|
||||
async handleIncomingCommand(@Body() dto: IncomingCommandDto): Promise<CommandResponse> {
|
||||
this.logger.log(`Received command from ${dto.instanceId}: ${dto.messageId}`);
|
||||
|
||||
return this.commandService.handleIncomingCommand(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all command messages for the workspace
|
||||
* Requires authentication
|
||||
*/
|
||||
@Get("commands")
|
||||
@UseGuards(AuthGuard)
|
||||
async getCommands(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Query("status") status?: FederationMessageStatus
|
||||
): Promise<CommandMessageDetails[]> {
|
||||
if (!req.user?.workspaceId) {
|
||||
throw new Error("Workspace ID not found in request");
|
||||
}
|
||||
|
||||
return this.commandService.getCommandMessages(req.user.workspaceId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single command message
|
||||
* Requires authentication
|
||||
*/
|
||||
@Get("commands/:id")
|
||||
@UseGuards(AuthGuard)
|
||||
async getCommand(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@Param("id") messageId: string
|
||||
): Promise<CommandMessageDetails> {
|
||||
if (!req.user?.workspaceId) {
|
||||
throw new Error("Workspace ID not found in request");
|
||||
}
|
||||
|
||||
return this.commandService.getCommandMessage(req.user.workspaceId, messageId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user