mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
Headers,
|
|
HttpException,
|
|
Param,
|
|
Post,
|
|
Put,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import type { User } from '../../types';
|
|
import { TodoService } from './todo.service';
|
|
import { TodoCreateItemDto, TodoUpdateItemDto, TodoReorderDto, TodoCategoryAssigneesDto } from './todo.dto';
|
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
|
import { CurrentUser } from '../auth/current-user.decorator';
|
|
import { RequirePermission, TripAccessGuard } from '../permissions/trip-access.guard';
|
|
|
|
/**
|
|
* /api/trips/:tripId/todo — trip-scoped task list.
|
|
*
|
|
* Every handler verifies trip access (404); mutations check the 'packing_edit'
|
|
* permission (403); create is 201, the rest 200; mutations broadcast over
|
|
* WebSocket with the forwarded X-Socket-Id. /reorder is declared before /:id so
|
|
* it wins over the param. Bodies validate against the @trek/shared todo schemas
|
|
* via the DTO classes in todo.dto.ts + the global ZodValidationPipe (400 with
|
|
* the standard `{ error }` envelope on mismatch — this replaced the legacy
|
|
* bespoke 'Item name is required' check).
|
|
*/
|
|
@Controller('api/trips/:tripId/todo')
|
|
// TripAccessGuard resolves :tripId and 404s a trip the user cannot reach; mutations
|
|
// add @RequirePermission('packing_edit'), the same action string the service's canEdit
|
|
// passes, so the HTTP and MCP paths cannot demand different rights.
|
|
@UseGuards(JwtAuthGuard, TripAccessGuard)
|
|
export class TodoController {
|
|
constructor(private readonly todo: TodoService) {}
|
|
|
|
|
|
|
|
@Get()
|
|
async list(@CurrentUser() user: User, @Param('tripId') tripId: string) {
|
|
return { items: await this.todo.listItems(tripId) };
|
|
}
|
|
|
|
@RequirePermission('packing_edit')
|
|
@Post()
|
|
async create(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Body() body: TodoCreateItemDto,
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const { name, category, due_date, description, assigned_user_id, priority } = body;
|
|
const item = await this.todo.createItem(tripId, { name, category, due_date, description, assigned_user_id, priority });
|
|
this.todo.broadcast(tripId, 'todo:created', { item }, socketId);
|
|
return { item };
|
|
}
|
|
|
|
@RequirePermission('packing_edit')
|
|
@Put('reorder')
|
|
async reorder(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Body() body: TodoReorderDto,
|
|
) {
|
|
await this.todo.reorderItems(tripId, body.orderedIds);
|
|
return { success: true };
|
|
}
|
|
|
|
@RequirePermission('packing_edit')
|
|
@Put(':id')
|
|
async update(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Param('id') id: string,
|
|
@Body() body: TodoUpdateItemDto,
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const { name, checked, category, due_date, description, assigned_user_id, priority } = body;
|
|
// bodyKeys carries which keys the request actually provided (the null-clear
|
|
// protocol); the parsed body only ever holds known schema keys.
|
|
const updated = await this.todo.updateItem(
|
|
tripId,
|
|
id,
|
|
// checked arrives as boolean or legacy 0/1 — normalize to the 0/1 the SQL binds.
|
|
{ name, checked: checked === undefined ? undefined : checked ? 1 : 0, category, due_date, description, assigned_user_id, priority },
|
|
Object.keys(body),
|
|
);
|
|
if (!updated) {
|
|
throw new HttpException({ error: 'Item not found' }, 404);
|
|
}
|
|
this.todo.broadcast(tripId, 'todo:updated', { item: updated }, socketId);
|
|
return { item: updated };
|
|
}
|
|
|
|
@RequirePermission('packing_edit')
|
|
@Delete(':id')
|
|
async remove(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Param('id') id: string,
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
if (!(await this.todo.deleteItem(tripId, id))) {
|
|
throw new HttpException({ error: 'Item not found' }, 404);
|
|
}
|
|
this.todo.broadcast(tripId, 'todo:deleted', { itemId: Number(id) }, socketId);
|
|
return { success: true };
|
|
}
|
|
|
|
@Get('category-assignees')
|
|
async categoryAssignees(@CurrentUser() user: User, @Param('tripId') tripId: string) {
|
|
return { assignees: await this.todo.getCategoryAssignees(tripId) };
|
|
}
|
|
|
|
@RequirePermission('packing_edit')
|
|
@Put('category-assignees/:categoryName')
|
|
async updateCategoryAssignees(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Param('categoryName') categoryName: string,
|
|
@Body() body: TodoCategoryAssigneesDto,
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const category = decodeURIComponent(categoryName);
|
|
const rows = await this.todo.updateCategoryAssignees(tripId, category, body.user_ids);
|
|
this.todo.broadcast(tripId, 'todo:assignees', { category, assignees: rows }, socketId);
|
|
return { assignees: rows };
|
|
}
|
|
}
|