/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { FastifyPluginAsync } from "fastify"; import { CreateLikeDto, LikeResponse, LikedItemDto, AuditAction, AuditCategory } from "@library/shared-types"; import { LikeService } from "../../services/like.service"; import { AuditService } from "../../services/audit.service"; import { bannedGuard } from "../../middleware/banned-guard"; const likesRoutes: FastifyPluginAsync = async (app) => { const likeService = new LikeService(); /** * Toggle like on an item (authenticated users). */ app.post<{ Body: CreateLikeDto; Reply: LikeResponse }>( "/toggle", { preValidation: [app.authenticate, bannedGuard], preHandler: [app.csrfProtection], }, async (request) => { const userId = request.user.id; const { entityType, entityId } = request.body; const result = await likeService.toggleLike(userId, entityType, entityId, request); return result; } ); /** * Get like count for an item (public route). */ app.get<{ Querystring: { entityType: string; entityId: string }; }>( "/count", async (request, reply) => { const { entityType, entityId } = request.query; // Validate entityType const validTypes = ['book', 'game', 'show', 'manga', 'music', 'art']; if (!validTypes.includes(entityType)) { reply.code(400); return { error: "Invalid entity type" }; } if (!entityId) { reply.code(400); return { error: "Entity ID is required" }; } const count = await likeService.getLikeCount(entityType as any, entityId); return { count }; } ); /** * Check if current user has liked an item (authenticated users). */ app.get<{ Querystring: { entityType: string; entityId: string }; }>( "/status", { preValidation: [app.authenticate], }, async (request, reply) => { const userId = request.user.id; const { entityType, entityId } = request.query; // Validate entityType const validTypes = ['book', 'game', 'show', 'manga', 'music', 'art']; if (!validTypes.includes(entityType)) { reply.code(400); return { error: "Invalid entity type" }; } if (!entityId) { reply.code(400); return { error: "Entity ID is required" }; } const liked = await likeService.getUserLikeStatus(userId, entityType as any, entityId); return { liked }; } ); /** * Get all items liked by the current user (authenticated users). */ app.get<{ Querystring: { entityType?: string }; }>( "/user", { preValidation: [app.authenticate], }, async (request, reply) => { const userId = request.user.id; const { entityType } = request.query; // Validate entityType if provided if (entityType) { const validTypes = ['book', 'game', 'show', 'manga', 'music', 'art']; if (!validTypes.includes(entityType)) { reply.code(400); return { error: "Invalid entity type" }; } } const likedItems = await likeService.getUserLikedItems(userId, entityType as any); return likedItems; } ); /** * Get bulk like statuses for multiple items (authenticated users). * Useful for efficiently loading like status for lists. */ app.post<{ Body: { items: Array<{ entityType: string; entityId: string }> }; }>( "/bulk-status", { preValidation: [app.authenticate], }, async (request, reply) => { const userId = request.user.id; const { items } = request.body; if (!items || !Array.isArray(items)) { reply.code(400); return { error: "Items array is required" }; } if (items.length > 100) { reply.code(400); return { error: "Maximum 100 items allowed per request" }; } const validTypes = ['book', 'game', 'show', 'manga', 'music', 'art']; const results = await Promise.all( items.map(async (item) => { if (!validTypes.includes(item.entityType)) { return { entityType: item.entityType, entityId: item.entityId, liked: false, count: 0, }; } const [liked, count] = await Promise.all([ likeService.getUserLikeStatus(userId, item.entityType as any, item.entityId), likeService.getLikeCount(item.entityType as any, item.entityId), ]); return { entityType: item.entityType, entityId: item.entityId, liked, count, }; }) ); return results; } ); }; export default likesRoutes;