dorfteich/apps/api/src/comments/comments.controller.ts
Claude Fable 5 4549d6d13f
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m19s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 4m55s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m5s
CD / Promote to Int (push) Successful in 10s
CI / Auth e2e pack (push) Successful in 5m20s
CI / Import/export fidelity gate (push) Successful in 45s
Add threaded page comments: data model, API, and comment policy (#91)
New comments table (thread via parent_id to the root, optional document
anchor on roots, resolved_at/by; page purge cascades, trash hides) with a
CommentsService enforcing the permission model: reading follows page
read, writing follows the new pond setting commentPolicy (readers |
editors) — 404 hides unreadable pages, 403 marks a failed write policy.
Endpoints: threaded list per page with an open/resolved filter (resolved
threads arrive collapsed by default), create (root or reply — replies
attach to roots only and carry no anchor), edit own, delete own (roots
with replies are admin-only, cascade), resolve/unresolve on roots for
everyone who may comment. Bodies are Markdown rendered through the shared
sanitizing pipeline; smuggled markup arrives as escaped text (fixture
test). The UI lands with #92.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 21:42:29 +02:00

86 lines
2.6 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Query,
Req,
} from '@nestjs/common';
import {
commentListQuerySchema,
createCommentInputSchema,
updateCommentInputSchema,
type CommentListQuery,
type CommentView,
type CreateCommentInput,
type PageCommentsView,
type UpdateCommentInput,
} from '@dorfteich/shared';
import { AuthedRequest } from '../auth/auth.guard';
import { ZodValidationPipe } from '../common/zod-validation.pipe';
import { AuthenticatedOnly, RequiresPagePermission } from '../permissions/permission.decorators';
import { CommentsService } from './comments.service';
/**
* Threaded page comments (issue #91). The page-scoped routes prove page
* read through the shared guard; the comment-scoped ones resolve their
* page (and the 404-vs-403 semantics) inside the service.
*/
@Controller()
export class CommentsController {
constructor(private readonly comments: CommentsService) {}
@Get('pages/:pageId/comments')
@RequiresPagePermission('read', { idParam: 'pageId' })
async list(
@Param('pageId') pageId: string,
@Query(new ZodValidationPipe(commentListQuerySchema)) query: CommentListQuery,
): Promise<PageCommentsView> {
return this.comments.list(pageId, query.filter);
}
@Post('pages/:pageId/comments')
@RequiresPagePermission('read', { idParam: 'pageId' }) // write policy: service
async create(
@Param('pageId') pageId: string,
@Body(new ZodValidationPipe(createCommentInputSchema)) input: CreateCommentInput,
@Req() request: AuthedRequest,
): Promise<CommentView> {
return this.comments.create(request.user!, pageId, input);
}
@Patch('comments/:id')
@AuthenticatedOnly()
async update(
@Param('id') id: string,
@Body(new ZodValidationPipe(updateCommentInputSchema)) input: UpdateCommentInput,
@Req() request: AuthedRequest,
): Promise<CommentView> {
return this.comments.update(request.user!, id, input.body);
}
@Delete('comments/:id')
@HttpCode(204)
@AuthenticatedOnly()
async remove(@Param('id') id: string, @Req() request: AuthedRequest): Promise<void> {
await this.comments.delete(request.user!, id);
}
@Post('comments/:id/resolve')
@AuthenticatedOnly()
async resolve(@Param('id') id: string, @Req() request: AuthedRequest): Promise<CommentView> {
return this.comments.setResolved(request.user!, id, true);
}
@Delete('comments/:id/resolve')
@AuthenticatedOnly()
async unresolve(@Param('id') id: string, @Req() request: AuthedRequest): Promise<CommentView> {
return this.comments.setResolved(request.user!, id, false);
}
}