/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { FastifyPluginAsync } from "fastify"; import { Art, CreateArtDto, UpdateArtDto, Comment, CreateCommentDto, AuditAction, AuditCategory } from "@library/shared-types"; import { ArtService } from "../../services/art.service"; import { CommentService } from "../../services/comment.service"; import { AuditService } from "../../services/audit.service"; import { adminGuard } from "../../middleware/admin-guard"; import { bannedGuard } from "../../middleware/banned-guard"; const artRoutes: FastifyPluginAsync = async (app) => { const artService = new ArtService(); const commentService = new CommentService(); /** * Get all art (public route). */ app.get<{ Reply: Art[] }>("/", async () => { return artService.getAllArt(); }); /** * Get single art piece by ID (public route). */ app.get<{ Params: { id: string }; Reply: Art | null }>( "/:id", async (request) => { const { id } = request.params; return artService.getArtById(id); } ); /** * Create new art piece (admin only). */ app.post<{ Body: CreateArtDto; Reply: Art }>( "/", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request) => { const art = await artService.createArt(request.body); await AuditService.logFromRequest(request, { action: AuditAction.ENTRY_CREATE, category: AuditCategory.CONTENT, resourceType: "art", resourceId: art.id, details: `Created art: ${art.title}`, }); return art; } ); /** * Update art by ID (admin only). */ app.put<{ Params: { id: string }; Body: UpdateArtDto; Reply: Art | null; }>( "/:id", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request) => { const { id } = request.params; const art = await artService.updateArt(id, request.body); if (art) { await AuditService.logFromRequest(request, { action: AuditAction.ENTRY_UPDATE, category: AuditCategory.CONTENT, resourceType: "art", resourceId: id, details: `Updated art: ${art.title}`, }); } return art; } ); /** * Delete art by ID (admin only). */ app.delete<{ Params: { id: string }; Reply: { success: boolean } }>( "/:id", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request) => { const { id } = request.params; await artService.deleteArt(id); await AuditService.logFromRequest(request, { action: AuditAction.ENTRY_DELETE, category: AuditCategory.CONTENT, resourceType: "art", resourceId: id, details: `Deleted art with ID: ${id}`, }); return { success: true }; } ); /** * Get comments for an art piece (public route). */ app.get<{ Params: { id: string }; Reply: Comment[] }>( "/:id/comments", async (request) => { const { id } = request.params; return commentService.getCommentsForArt(id); } ); /** * Add comment to an art piece (authenticated users). */ app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>( "/:id/comments", { preValidation: [app.authenticate, bannedGuard], preHandler: [app.csrfProtection], }, async (request) => { const { id } = request.params; const userId = request.user.id; const comment = await commentService.createCommentForArt(id, userId, request.body); await AuditService.logFromRequest(request, { action: AuditAction.COMMENT_CREATE, category: AuditCategory.CONTENT, resourceType: "art", resourceId: id, details: `Added comment to art`, }); return comment; } ); /** * Update comment (owner or admin). */ app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>( "/:id/comments/:commentId", { preValidation: [app.authenticate], preHandler: [app.csrfProtection], }, async (request, reply) => { const { id, commentId } = request.params; const userId = request.user.id; const isAdmin = request.user.isAdmin; const verification = await commentService.verifyCommentOwnership(commentId, "art", id); if (!verification.exists) { return reply.code(404).send({ error: "Comment not found" }); } if (verification.comment?.userId !== userId && !isAdmin) { return reply.code(403).send({ error: "You can only edit your own comments" }); } const comment = await commentService.updateComment(commentId, request.body.content); await AuditService.logFromRequest(request, { action: AuditAction.COMMENT_UPDATE, category: AuditCategory.CONTENT, resourceType: "art", resourceId: id, details: `Updated comment ${commentId} on art`, }); return comment; } ); /** * Delete comment (owner or admin). */ app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>( "/:id/comments/:commentId", { preValidation: [app.authenticate], preHandler: [app.csrfProtection], }, async (request, reply) => { const { id, commentId } = request.params; const userId = request.user.id; const isAdmin = request.user.isAdmin; const verification = await commentService.verifyCommentOwnership(commentId, "art", id); if (!verification.exists) { return reply.code(404).send({ error: "Comment not found" }); } if (verification.comment?.userId !== userId && !isAdmin) { return reply.code(403).send({ error: "You can only delete your own comments" }); } await commentService.deleteComment(commentId); await AuditService.logFromRequest(request, { action: AuditAction.COMMENT_DELETE, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, resourceType: "art", resourceId: id, details: `Deleted comment ${commentId} from art`, }); return { success: true }; } ); }; export default artRoutes;