import { Body, Controller, Delete, ForbiddenException, Get, HttpCode, HttpStatus, Inject, NotFoundException, Param, Patch, Post, Query, UseGuards, } from '@nestjs/common'; 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 { assertOwner } from '../auth/resource-ownership.js'; import { CreateTaskDto, UpdateTaskDto } from './tasks.dto.js'; @Controller('api/tasks') @UseGuards(AuthGuard) export class TasksController { constructor(@Inject(BRAIN) private readonly brain: Brain) {} @Get() async list( @CurrentUser() user: { id: string }, @Query('projectId') projectId?: string, @Query('missionId') missionId?: string, @Query('status') status?: string, ) { if (projectId) { await this.getOwnedProject(projectId, user.id, 'Task'); return this.brain.tasks.findByProject(projectId); } if (missionId) { await this.getOwnedMission(missionId, user.id, 'Task'); return this.brain.tasks.findByMission(missionId); } const [projects, missions, tasks] = await Promise.all([ this.brain.projects.findAll(), this.brain.missions.findAll(), status ? this.brain.tasks.findByStatus( status as Parameters[0], ) : this.brain.tasks.findAll(), ]); const ownedProjectIds = new Set( projects.filter((project) => project.ownerId === user.id).map((project) => project.id), ); const ownedMissionIds = new Set( missions .filter( (ownedMission) => typeof ownedMission.projectId === 'string' && ownedProjectIds.has(ownedMission.projectId), ) .map((ownedMission) => ownedMission.id), ); return tasks.filter( (task) => (task.projectId ? ownedProjectIds.has(task.projectId) : false) || (task.missionId ? ownedMissionIds.has(task.missionId) : false), ); } @Get(':id') async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) { return this.getOwnedTask(id, user.id); } @Post() async create(@Body() dto: CreateTaskDto, @CurrentUser() user: { id: string }) { if (dto.projectId) { await this.getOwnedProject(dto.projectId, user.id, 'Task'); } if (dto.missionId) { await this.getOwnedMission(dto.missionId, user.id, 'Task'); } return this.brain.tasks.create({ title: dto.title, description: dto.description, status: dto.status, priority: dto.priority, projectId: dto.projectId, missionId: dto.missionId, assignee: dto.assignee, tags: dto.tags, dueDate: dto.dueDate ? new Date(dto.dueDate) : undefined, }); } @Patch(':id') async update( @Param('id') id: string, @Body() dto: UpdateTaskDto, @CurrentUser() user: { id: string }, ) { await this.getOwnedTask(id, user.id); if (dto.projectId) { await this.getOwnedProject(dto.projectId, user.id, 'Task'); } if (dto.missionId) { await this.getOwnedMission(dto.missionId, user.id, 'Task'); } const task = await this.brain.tasks.update(id, { ...dto, dueDate: dto.dueDate ? new Date(dto.dueDate) : dto.dueDate === null ? null : undefined, }); if (!task) throw new NotFoundException('Task not found'); return task; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) { await this.getOwnedTask(id, user.id); const deleted = await this.brain.tasks.remove(id); if (!deleted) throw new NotFoundException('Task not found'); } private async getOwnedTask(id: string, userId: string) { const task = await this.brain.tasks.findById(id); if (!task) throw new NotFoundException('Task not found'); if (task.projectId) { await this.getOwnedProject(task.projectId, userId, 'Task'); return task; } if (task.missionId) { await this.getOwnedMission(task.missionId, userId, 'Task'); return task; } throw new ForbiddenException('Task does not belong to the current user'); } private async getOwnedMission(missionId: string, userId: string, resourceName: string) { const mission = await this.brain.missions.findById(missionId); if (!mission?.projectId) { throw new ForbiddenException(`${resourceName} does not belong to the current user`); } await this.getOwnedProject(mission.projectId, userId, resourceName); return mission; } private async getOwnedProject(projectId: string, userId: string, resourceName: string) { const project = await this.brain.projects.findById(projectId); if (!project) { throw new ForbiddenException(`${resourceName} does not belong to the current user`); } assertOwner(project.ownerId, userId, resourceName); return project; } }