generated from nhcarrigan/template
175 lines
4.7 KiB
TypeScript
175 lines
4.7 KiB
TypeScript
/**
|
|
* @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; |