Files
TREK/server/src/nest/day-notes/day-notes.controller.ts
T

101 lines
3.5 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
Headers,
HttpException,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import type { User } from '../../types';
import { DayNotesService } from './day-notes.service';
import { DayNoteCreateDto, DayNoteUpdateDto } from './day-notes.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/days/:dayId/notes — free-text annotations on a day.
*
* Bodies validate against the @trek/shared day-note contracts via the global
* ZodValidationPipe (which, like the legacy string-length guard, 400s before
* the trip-access check), then trip access (404), then the 'day_edit'
* permission (403); create 201 / rest 200; the bespoke "Day not found" /
* "Note not found" / "Text required" bodies; WebSocket broadcasts with the
* forwarded X-Socket-Id.
*/
@Controller('api/trips/:tripId/days/:dayId/notes')
// TripAccessGuard resolves :tripId and 404s a trip the user cannot reach; mutations
// add @RequirePermission('day_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 DayNotesController {
constructor(private readonly notes: DayNotesService) {}
@Get()
async list(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('dayId') dayId: string) {
return { notes: await this.notes.list(dayId, tripId) };
}
@RequirePermission('day_edit')
@Post()
async create(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('dayId') dayId: string,
@Body() body: DayNoteCreateDto,
@Headers('x-socket-id') socketId?: string,
) {
if (!(await this.notes.dayExists(dayId, tripId))) {
throw new HttpException({ error: 'Day not found' }, 404);
}
if (!body.text?.trim()) {
throw new HttpException({ error: 'Text required' }, 400);
}
const note = await this.notes.create(dayId, tripId, body.text, body.time, body.icon, body.sort_order, body.color);
this.notes.broadcast(tripId, 'dayNote:created', { dayId: Number(dayId), note }, socketId);
return { note };
}
@RequirePermission('day_edit')
@Put(':id')
async update(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('dayId') dayId: string,
@Param('id') id: string,
@Body() body: DayNoteUpdateDto,
@Headers('x-socket-id') socketId?: string,
) {
const current = await this.notes.getNote(id, dayId, tripId);
if (!current) {
throw new HttpException({ error: 'Note not found' }, 404);
}
const note = await this.notes.update(id, current as never, { text: body.text, time: body.time, icon: body.icon, sort_order: body.sort_order, color: body.color });
this.notes.broadcast(tripId, 'dayNote:updated', { dayId: Number(dayId), note }, socketId);
return { note };
}
@RequirePermission('day_edit')
@Delete(':id')
async remove(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('dayId') dayId: string,
@Param('id') id: string,
@Headers('x-socket-id') socketId?: string,
) {
if (!(await this.notes.getNote(id, dayId, tripId))) {
throw new HttpException({ error: 'Note not found' }, 404);
}
await this.notes.remove(id);
this.notes.broadcast(tripId, 'dayNote:deleted', { noteId: Number(id), dayId: Number(dayId) }, socketId);
return { success: true };
}
}