Files
library/api/src/app/routes/books/index.ts
T

219 lines
6.4 KiB
TypeScript

/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import { Book, CreateBookDto, UpdateBookDto, Comment, CreateCommentDto, AuditAction, AuditCategory } from "@library/shared-types";
import { BookService } from "../../services/book.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 booksRoutes: FastifyPluginAsync = async (app) => {
const bookService = new BookService();
const commentService = new CommentService();
/**
* Get all books (public route).
*/
app.get<{ Reply: Book[] }>("/", async () => {
return bookService.getAllBooks();
});
/**
* Get single book by ID (public route).
*/
app.get<{ Params: { id: string }; Reply: Book | null }>(
"/:id",
async (request) => {
const { id } = request.params;
return bookService.getBookById(id);
}
);
/**
* Create new book (admin only).
*/
app.post<{ Body: CreateBookDto; Reply: Book }>(
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const book = await bookService.createBook(request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE,
category: AuditCategory.CONTENT,
resourceType: "book",
resourceId: book.id,
details: `Created book: ${book.title}`,
});
return book;
}
);
/**
* Update book by ID (admin only).
*/
app.put<{
Params: { id: string };
Body: UpdateBookDto;
Reply: Book | null;
}>(
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const book = await bookService.updateBook(id, request.body);
if (book) {
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE,
category: AuditCategory.CONTENT,
resourceType: "book",
resourceId: id,
details: `Updated book: ${book.title}`,
});
}
return book;
}
);
/**
* Delete book 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 bookService.deleteBook(id);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE,
category: AuditCategory.CONTENT,
resourceType: "book",
resourceId: id,
details: `Deleted book with ID: ${id}`,
});
return { success: true };
}
);
/**
* Get comments for a book (public route).
*/
app.get<{ Params: { id: string }; Reply: Comment[] }>(
"/:id/comments",
async (request) => {
const { id } = request.params;
return commentService.getCommentsForBook(id);
}
);
/**
* Add comment to a book (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.createCommentForBook(id, userId, request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE,
category: AuditCategory.CONTENT,
resourceType: "book",
resourceId: id,
details: `Added comment to book`,
});
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, "book", 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: "book",
resourceId: id,
details: `Updated comment ${commentId} on book`,
});
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, "book", 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: "book",
resourceId: id,
details: `Deleted comment ${commentId} from book`,
});
return { success: true };
}
);
};
export default booksRoutes;