feat: security and auditing

This commit is contained in:
2026-02-04 16:48:08 -08:00
parent 11be34cd21
commit 0a654f423a
42 changed files with 2195 additions and 160 deletions
+53 -6
View File
@@ -5,10 +5,12 @@
*/
import { FastifyPluginAsync } from "fastify";
import { Art, CreateArtDto, UpdateArtDto, Comment, CreateCommentDto } from "@library/shared-types";
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();
@@ -39,9 +41,18 @@ const artRoutes: FastifyPluginAsync = async (app) => {
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
return artService.createArt(request.body);
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;
}
);
@@ -56,10 +67,21 @@ const artRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
return artService.updateArt(id, request.body);
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;
}
);
@@ -70,10 +92,18 @@ const artRoutes: FastifyPluginAsync = async (app) => {
"/: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 };
}
);
@@ -95,12 +125,21 @@ const artRoutes: FastifyPluginAsync = async (app) => {
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
preValidation: [app.authenticate, bannedGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForArt(id, userId, request.body);
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;
}
);
@@ -111,10 +150,18 @@ const artRoutes: FastifyPluginAsync = async (app) => {
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { commentId } = request.params;
const { id, commentId } = request.params;
await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE,
category: AuditCategory.ADMIN,
resourceType: "art",
resourceId: id,
details: `Deleted comment ${commentId} from art`,
});
return { success: true };
}
);
+85
View File
@@ -0,0 +1,85 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import { AuditService } from "../../services/audit.service";
import { adminGuard } from "../../middleware/admin-guard";
import type { AuditAction, AuditCategory } from "@library/shared-types";
interface AuditLogQuery {
action?: AuditAction;
category?: AuditCategory;
userId?: string;
success?: string;
startDate?: string;
endDate?: string;
page?: string;
limit?: string;
}
const auditRoutes: FastifyPluginAsync = async (app) => {
/**
* Get audit logs (admin only).
*/
app.get<{ Querystring: AuditLogQuery }>(
"/",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { action, category, userId, success, startDate, endDate, page, limit } = request.query;
return AuditService.getLogs({
action: action as AuditAction | undefined,
category: category as AuditCategory | undefined,
userId,
success: success === undefined ? undefined : success === "true",
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
page: page ? parseInt(page, 10) : 1,
limit: limit ? parseInt(limit, 10) : 50,
});
}
);
/**
* Get security logs (admin only).
*/
app.get<{ Querystring: { page?: string; limit?: string } }>(
"/security",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { page, limit } = request.query;
return AuditService.getSecurityLogs(
page ? parseInt(page, 10) : 1,
limit ? parseInt(limit, 10) : 50
);
}
);
/**
* Get logs for a specific user (admin only).
*/
app.get<{ Params: { userId: string }; Querystring: { page?: string; limit?: string } }>(
"/user/:userId",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { userId } = request.params;
const { page, limit } = request.query;
return AuditService.getLogsByUser(
userId,
page ? parseInt(page, 10) : 1,
limit ? parseInt(limit, 10) : 50
);
}
);
};
export default auditRoutes;
+56 -5
View File
@@ -1,6 +1,7 @@
import { FastifyPluginAsync } from "fastify";
import { AuthService } from "../../services/auth.service";
import { AuthResponse } from "@library/shared-types";
import { AuditService } from "../../services/audit.service";
import { AuthResponse, AuditAction, AuditCategory } from "@library/shared-types";
const authRoutes: FastifyPluginAsync = async (app) => {
const authService = new AuthService(app);
@@ -33,7 +34,16 @@ const authRoutes: FastifyPluginAsync = async (app) => {
// Generate JWT
const jwt = await authService.generateToken(user);
// Set cookie and redirect to frontend
// Log successful login
await AuditService.log({
action: AuditAction.LOGIN,
category: AuditCategory.AUTH,
userId: user.id,
details: `User ${user.username} logged in via Discord`,
success: true,
}, request);
// Set signed cookie and redirect to frontend
reply
.setCookie("auth-token", jwt, {
path: "/",
@@ -41,13 +51,22 @@ const authRoutes: FastifyPluginAsync = async (app) => {
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60, // 7 days
signed: true,
})
.redirect("/"); // Redirect to root since API serves frontend
} catch (error) {
// Log failed login attempt
await AuditService.log({
action: AuditAction.LOGIN_FAILED,
category: AuditCategory.SECURITY,
details: error instanceof Error ? error.message : String(error),
success: false,
}, request);
app.log.error({ err: error }, "Auth callback error");
reply
.code(401)
.send({ error: "Authentication failed", details: error instanceof Error ? error.message : String(error) });
.send({ error: "Authentication failed" });
}
});
@@ -59,8 +78,14 @@ const authRoutes: FastifyPluginAsync = async (app) => {
{
preValidation: [app.authenticate],
},
async (request) => {
const user = request.user as any;
async (request, reply) => {
const jwtUser = request.user as { id: string };
const user = await authService.getUserById(jwtUser.id);
if (!user) {
return reply.code(404).send({ error: "User not found" });
}
const token = await authService.generateToken(user);
return {
@@ -74,12 +99,38 @@ const authRoutes: FastifyPluginAsync = async (app) => {
* Logout.
*/
app.post("/logout", async (request, reply) => {
// Try to get user ID from JWT if available
try {
await request.jwtVerify();
const user = request.user as { id?: string; username?: string };
if (user?.id) {
await AuditService.log({
action: AuditAction.LOGOUT,
category: AuditCategory.AUTH,
userId: user.id,
details: `User ${user.username ?? "unknown"} logged out`,
success: true,
}, request);
}
} catch {
// User wasn't authenticated, just proceed with logout
}
reply
.clearCookie("auth-token", {
path: "/",
signed: true,
})
.send({ message: "Logged out successfully" });
});
/**
* Get CSRF token for state-changing requests.
*/
app.get("/csrf-token", async (request, reply) => {
const token = reply.generateCsrf();
return { csrfToken: token };
});
};
export default authRoutes;
+53 -6
View File
@@ -5,10 +5,12 @@
*/
import { FastifyPluginAsync } from "fastify";
import { Book, CreateBookDto, UpdateBookDto, Comment, CreateCommentDto } from "@library/shared-types";
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();
@@ -39,9 +41,18 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
return bookService.createBook(request.body);
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;
}
);
@@ -56,10 +67,21 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
return bookService.updateBook(id, request.body);
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;
}
);
@@ -70,10 +92,18 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
"/: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 };
}
);
@@ -95,12 +125,21 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
preValidation: [app.authenticate, bannedGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForBook(id, userId, request.body);
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;
}
);
@@ -111,10 +150,18 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { commentId } = request.params;
const { id, commentId } = request.params;
await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE,
category: AuditCategory.ADMIN,
resourceType: "book",
resourceId: id,
details: `Deleted comment ${commentId} from book`,
});
return { success: true };
}
);
+53 -6
View File
@@ -5,10 +5,12 @@
*/
import { FastifyPluginAsync } from "fastify";
import { Game, CreateGameDto, UpdateGameDto, Comment, CreateCommentDto } from "@library/shared-types";
import { Game, CreateGameDto, UpdateGameDto, Comment, CreateCommentDto, AuditAction, AuditCategory } from "@library/shared-types";
import { GameService } from "../../services/game.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 gamesRoutes: FastifyPluginAsync = async (app) => {
const gameService = new GameService();
@@ -33,9 +35,18 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
return gameService.createGame(request.body);
const game = await gameService.createGame(request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE,
category: AuditCategory.CONTENT,
resourceType: "game",
resourceId: game.id,
details: `Created game: ${game.title}`,
});
return game;
}
);
@@ -48,10 +59,21 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
return gameService.updateGame(id, request.body);
const game = await gameService.updateGame(id, request.body);
if (game) {
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE,
category: AuditCategory.CONTENT,
resourceType: "game",
resourceId: id,
details: `Updated game: ${game.title}`,
});
}
return game;
}
);
@@ -60,10 +82,18 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
await gameService.deleteGame(id);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE,
category: AuditCategory.CONTENT,
resourceType: "game",
resourceId: id,
details: `Deleted game with ID: ${id}`,
});
return { success: true };
}
);
@@ -81,12 +111,21 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
preValidation: [app.authenticate, bannedGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForGame(id, userId, request.body);
const comment = await commentService.createCommentForGame(id, userId, request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE,
category: AuditCategory.CONTENT,
resourceType: "game",
resourceId: id,
details: `Added comment to game`,
});
return comment;
}
);
@@ -95,10 +134,18 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { commentId } = request.params;
const { id, commentId } = request.params;
await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE,
category: AuditCategory.ADMIN,
resourceType: "game",
resourceId: id,
details: `Deleted comment ${commentId} from game`,
});
return { success: true };
}
);
+53 -6
View File
@@ -5,10 +5,12 @@
*/
import { FastifyPluginAsync } from "fastify";
import { Manga, CreateMangaDto, UpdateMangaDto, Comment, CreateCommentDto } from "@library/shared-types";
import { Manga, CreateMangaDto, UpdateMangaDto, Comment, CreateCommentDto, AuditAction, AuditCategory } from "@library/shared-types";
import { MangaService } from "../../services/manga.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 mangaRoutes: FastifyPluginAsync = async (app) => {
const mangaService = new MangaService();
@@ -30,9 +32,18 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
return mangaService.createManga(request.body);
const manga = await mangaService.createManga(request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE,
category: AuditCategory.CONTENT,
resourceType: "manga",
resourceId: manga.id,
details: `Created manga: ${manga.title}`,
});
return manga;
}
);
@@ -44,10 +55,21 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
return mangaService.updateManga(id, request.body);
const manga = await mangaService.updateManga(id, request.body);
if (manga) {
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE,
category: AuditCategory.CONTENT,
resourceType: "manga",
resourceId: id,
details: `Updated manga: ${manga.title}`,
});
}
return manga;
}
);
@@ -55,10 +77,18 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
await mangaService.deleteManga(id);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE,
category: AuditCategory.CONTENT,
resourceType: "manga",
resourceId: id,
details: `Deleted manga with ID: ${id}`,
});
return { success: true };
}
);
@@ -74,12 +104,21 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
preValidation: [app.authenticate, bannedGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForManga(id, userId, request.body);
const comment = await commentService.createCommentForManga(id, userId, request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE,
category: AuditCategory.CONTENT,
resourceType: "manga",
resourceId: id,
details: `Added comment to manga`,
});
return comment;
}
);
@@ -87,10 +126,18 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { commentId } = request.params;
const { id, commentId } = request.params;
await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE,
category: AuditCategory.ADMIN,
resourceType: "manga",
resourceId: id,
details: `Deleted comment ${commentId} from manga`,
});
return { success: true };
}
);
+53 -6
View File
@@ -5,10 +5,12 @@
*/
import { FastifyPluginAsync } from "fastify";
import { Music, CreateMusicDto, UpdateMusicDto, Comment, CreateCommentDto } from "@library/shared-types";
import { Music, CreateMusicDto, UpdateMusicDto, Comment, CreateCommentDto, AuditAction, AuditCategory } from "@library/shared-types";
import { MusicService } from "../../services/music.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 musicRoutes: FastifyPluginAsync = async (app) => {
const musicService = new MusicService();
@@ -39,9 +41,18 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
return musicService.createMusic(request.body);
const music = await musicService.createMusic(request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE,
category: AuditCategory.CONTENT,
resourceType: "music",
resourceId: music.id,
details: `Created music: ${music.title}`,
});
return music;
}
);
@@ -56,10 +67,21 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
return musicService.updateMusic(id, request.body);
const music = await musicService.updateMusic(id, request.body);
if (music) {
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE,
category: AuditCategory.CONTENT,
resourceType: "music",
resourceId: id,
details: `Updated music: ${music.title}`,
});
}
return music;
}
);
@@ -70,10 +92,18 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
await musicService.deleteMusic(id);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE,
category: AuditCategory.CONTENT,
resourceType: "music",
resourceId: id,
details: `Deleted music with ID: ${id}`,
});
return { success: true };
}
);
@@ -95,12 +125,21 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
preValidation: [app.authenticate, bannedGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForMusic(id, userId, request.body);
const comment = await commentService.createCommentForMusic(id, userId, request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE,
category: AuditCategory.CONTENT,
resourceType: "music",
resourceId: id,
details: `Added comment to music`,
});
return comment;
}
);
@@ -111,10 +150,18 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { commentId } = request.params;
const { id, commentId } = request.params;
await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE,
category: AuditCategory.ADMIN,
resourceType: "music",
resourceId: id,
details: `Deleted comment ${commentId} from music`,
});
return { success: true };
}
);
+53 -6
View File
@@ -5,10 +5,12 @@
*/
import { FastifyPluginAsync } from "fastify";
import { Show, CreateShowDto, UpdateShowDto, Comment, CreateCommentDto } from "@library/shared-types";
import { Show, CreateShowDto, UpdateShowDto, Comment, CreateCommentDto, AuditAction, AuditCategory } from "@library/shared-types";
import { ShowService } from "../../services/show.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 showsRoutes: FastifyPluginAsync = async (app) => {
const showService = new ShowService();
@@ -30,9 +32,18 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
"/",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
return showService.createShow(request.body);
const show = await showService.createShow(request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE,
category: AuditCategory.CONTENT,
resourceType: "show",
resourceId: show.id,
details: `Created show: ${show.title}`,
});
return show;
}
);
@@ -44,10 +55,21 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
return showService.updateShow(id, request.body);
const show = await showService.updateShow(id, request.body);
if (show) {
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE,
category: AuditCategory.CONTENT,
resourceType: "show",
resourceId: id,
details: `Updated show: ${show.title}`,
});
}
return show;
}
);
@@ -55,10 +77,18 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
"/:id",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
await showService.deleteShow(id);
await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE,
category: AuditCategory.CONTENT,
resourceType: "show",
resourceId: id,
details: `Deleted show with ID: ${id}`,
});
return { success: true };
}
);
@@ -74,12 +104,21 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
preValidation: [app.authenticate, bannedGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForShow(id, userId, request.body);
const comment = await commentService.createCommentForShow(id, userId, request.body);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE,
category: AuditCategory.CONTENT,
resourceType: "show",
resourceId: id,
details: `Added comment to show`,
});
return comment;
}
);
@@ -87,10 +126,18 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request) => {
const { commentId } = request.params;
const { id, commentId } = request.params;
await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE,
category: AuditCategory.ADMIN,
resourceType: "show",
resourceId: id,
details: `Deleted comment ${commentId} from show`,
});
return { success: true };
}
);
+92
View File
@@ -0,0 +1,92 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import { User, AuditAction, AuditCategory } from "@library/shared-types";
import { UserService } from "../../services/user.service";
import { AuditService } from "../../services/audit.service";
import { adminGuard } from "../../middleware/admin-guard";
const usersRoutes: FastifyPluginAsync = async (app) => {
const userService = new UserService();
app.get<{ Reply: User[] }>(
"/",
{
preValidation: [app.authenticate, adminGuard],
},
async () => {
return userService.getAllUsers();
}
);
app.get<{ Params: { id: string }; Reply: User | null }>(
"/:id",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { id } = request.params;
return userService.getUserById(id);
}
);
app.post<{ Params: { id: string }; Reply: User | { error: string } }>(
"/:id/ban",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request, reply) => {
const { id } = request.params;
const currentUser = request.user as { id: string };
if (currentUser.id === id) {
return reply.code(400).send({ error: "You cannot ban yourself" });
}
const user = await userService.banUser(id);
if (!user) {
return reply.code(404).send({ error: "User not found" });
}
await AuditService.logFromRequest(request, {
action: AuditAction.USER_BAN,
category: AuditCategory.ADMIN,
targetUserId: id,
details: `Banned user: ${user.username}`,
});
return user;
}
);
app.post<{ Params: { id: string }; Reply: User | { error: string } }>(
"/:id/unban",
{
preValidation: [app.authenticate, adminGuard],
preHandler: [app.csrfProtection],
},
async (request, reply) => {
const { id } = request.params;
const user = await userService.unbanUser(id);
if (!user) {
return reply.code(404).send({ error: "User not found" });
}
await AuditService.logFromRequest(request, {
action: AuditAction.USER_UNBAN,
category: AuditCategory.ADMIN,
targetUserId: id,
details: `Unbanned user: ${user.username}`,
});
return user;
}
);
};
export default usersRoutes;