feat: multiple improvements to library functionality #50

Merged
naomi merged 12 commits from feat/tickets into main 2026-02-19 16:52:43 -08:00
46 changed files with 531 additions and 463 deletions
Showing only changes of commit 9f0132db34 - Show all commits
+1
View File
@@ -7,4 +7,5 @@ module.exports = {
}, },
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../coverage/api', coverageDirectory: '../coverage/api',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
}; };
+1 -1
View File
@@ -15,6 +15,6 @@ describe('GET /', () => {
url: '/', url: '/',
}); });
expect(response.json()).toEqual({ message: 'Hello API' }); expect(response.json()).toEqual({ version: expect.any(String) });
}); });
}); });
+4 -4
View File
@@ -13,8 +13,8 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) {
// Log CSRF validation failures // Log CSRF validation failures
if (error.code === 'FST_CSRF_INVALID_TOKEN' || error.code === 'FST_CSRF_MISSING_SECRET') { if (error.code === 'FST_CSRF_INVALID_TOKEN' || error.code === 'FST_CSRF_MISSING_SECRET') {
await AuditService.log({ await AuditService.log({
action: AuditAction.CSRF_VALIDATION_FAILED, action: AuditAction.csrfValidationFailed,
category: AuditCategory.SECURITY, category: AuditCategory.security,
details: `CSRF validation failed: ${error.message}, URL: ${request.url}`, details: `CSRF validation failed: ${error.message}, URL: ${request.url}`,
success: false, success: false,
}, request).catch(() => { }, request).catch(() => {
@@ -25,8 +25,8 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) {
// Log unauthorized access attempts // Log unauthorized access attempts
if (error.statusCode === 401 || error.statusCode === 403) { if (error.statusCode === 401 || error.statusCode === 403) {
await AuditService.log({ await AuditService.log({
action: AuditAction.UNAUTHORIZED_ACCESS, action: AuditAction.unauthorizedAccess,
category: AuditCategory.SECURITY, category: AuditCategory.security,
details: `Unauthorized access attempt: ${error.message}, URL: ${request.url}`, details: `Unauthorized access attempt: ${error.message}, URL: ${request.url}`,
success: false, success: false,
}, request).catch(() => { }, request).catch(() => {
+3 -1
View File
@@ -82,7 +82,9 @@ const authPlugin: FastifyPluginAsync = async (app) => {
try { try {
await request.jwtVerify(); await request.jwtVerify();
} catch (err) { } catch (err) {
throw app.httpErrors.unauthorized("Invalid token"); const error = new Error("Invalid token");
(error as any).statusCode = 401;
throw error;
} }
}); });
}; };
+2 -2
View File
@@ -17,8 +17,8 @@ const rateLimitPlugin: FastifyPluginAsync = async (app) => {
errorResponseBuilder: (request) => { errorResponseBuilder: (request) => {
// Log rate limit exceeded event // Log rate limit exceeded event
AuditService.log({ AuditService.log({
action: AuditAction.RATE_LIMIT_EXCEEDED, action: AuditAction.rateLimitExceeded,
category: AuditCategory.SECURITY, category: AuditCategory.security,
details: `Rate limit exceeded for URL: ${request.url}`, details: `Rate limit exceeded for URL: ${request.url}`,
success: false, success: false,
}, request).catch(() => { }, request).catch(() => {
+12 -12
View File
@@ -46,8 +46,8 @@ const artRoutes: FastifyPluginAsync = async (app) => {
async (request) => { async (request) => {
const art = await artService.createArt(request.body); const art = await artService.createArt(request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "art", resourceType: "art",
resourceId: art.id, resourceId: art.id,
details: `Created art: ${art.title}`, details: `Created art: ${art.title}`,
@@ -74,8 +74,8 @@ const artRoutes: FastifyPluginAsync = async (app) => {
const art = await artService.updateArt(id, request.body); const art = await artService.updateArt(id, request.body);
if (art) { if (art) {
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "art", resourceType: "art",
resourceId: id, resourceId: id,
details: `Updated art: ${art.title}`, details: `Updated art: ${art.title}`,
@@ -98,8 +98,8 @@ const artRoutes: FastifyPluginAsync = async (app) => {
const { id } = request.params; const { id } = request.params;
await artService.deleteArt(id); await artService.deleteArt(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "art", resourceType: "art",
resourceId: id, resourceId: id,
details: `Deleted art with ID: ${id}`, details: `Deleted art with ID: ${id}`,
@@ -133,8 +133,8 @@ const artRoutes: FastifyPluginAsync = async (app) => {
const userId = request.user.id; const userId = request.user.id;
const comment = await commentService.createCommentForArt(id, userId, request.body); const comment = await commentService.createCommentForArt(id, userId, request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE, action: AuditAction.commentCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "art", resourceType: "art",
resourceId: id, resourceId: id,
details: `Added comment to art`, details: `Added comment to art`,
@@ -169,8 +169,8 @@ const artRoutes: FastifyPluginAsync = async (app) => {
const comment = await commentService.updateComment(commentId, request.body.content); const comment = await commentService.updateComment(commentId, request.body.content);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_UPDATE, action: AuditAction.commentUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "art", resourceType: "art",
resourceId: id, resourceId: id,
details: `Updated comment ${commentId} on art`, details: `Updated comment ${commentId} on art`,
@@ -205,8 +205,8 @@ const artRoutes: FastifyPluginAsync = async (app) => {
await commentService.deleteComment(commentId); await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE, action: AuditAction.commentDelete,
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content,
resourceType: "art", resourceType: "art",
resourceId: id, resourceId: id,
details: `Deleted comment ${commentId} from art`, details: `Deleted comment ${commentId} from art`,
+6 -6
View File
@@ -85,8 +85,8 @@ const authRoutes: FastifyPluginAsync = async (app) => {
// Log successful login // Log successful login
await AuditService.log({ await AuditService.log({
action: AuditAction.LOGIN, action: AuditAction.login,
category: AuditCategory.AUTH, category: AuditCategory.auth,
userId: user.id, userId: user.id,
details: `User ${user.username} logged in via Discord`, details: `User ${user.username} logged in via Discord`,
success: true, success: true,
@@ -114,8 +114,8 @@ const authRoutes: FastifyPluginAsync = async (app) => {
} catch (error) { } catch (error) {
// Log failed login attempt // Log failed login attempt
await AuditService.log({ await AuditService.log({
action: AuditAction.LOGIN_FAILED, action: AuditAction.loginFailed,
category: AuditCategory.SECURITY, category: AuditCategory.security,
details: error instanceof Error ? error.message : String(error), details: error instanceof Error ? error.message : String(error),
success: false, success: false,
}, request); }, request);
@@ -229,8 +229,8 @@ const authRoutes: FastifyPluginAsync = async (app) => {
const user = request.user as { id?: string; username?: string }; const user = request.user as { id?: string; username?: string };
if (user?.id) { if (user?.id) {
await AuditService.log({ await AuditService.log({
action: AuditAction.LOGOUT, action: AuditAction.logout,
category: AuditCategory.AUTH, category: AuditCategory.auth,
userId: user.id, userId: user.id,
details: `User ${user.username ?? "unknown"} logged out`, details: `User ${user.username ?? "unknown"} logged out`,
success: true, success: true,
+12 -12
View File
@@ -46,8 +46,8 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
async (request) => { async (request) => {
const book = await bookService.createBook(request.body); const book = await bookService.createBook(request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "book", resourceType: "book",
resourceId: book.id, resourceId: book.id,
details: `Created book: ${book.title}`, details: `Created book: ${book.title}`,
@@ -74,8 +74,8 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
const book = await bookService.updateBook(id, request.body); const book = await bookService.updateBook(id, request.body);
if (book) { if (book) {
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "book", resourceType: "book",
resourceId: id, resourceId: id,
details: `Updated book: ${book.title}`, details: `Updated book: ${book.title}`,
@@ -98,8 +98,8 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
const { id } = request.params; const { id } = request.params;
await bookService.deleteBook(id); await bookService.deleteBook(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "book", resourceType: "book",
resourceId: id, resourceId: id,
details: `Deleted book with ID: ${id}`, details: `Deleted book with ID: ${id}`,
@@ -133,8 +133,8 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
const userId = request.user.id; const userId = request.user.id;
const comment = await commentService.createCommentForBook(id, userId, request.body); const comment = await commentService.createCommentForBook(id, userId, request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE, action: AuditAction.commentCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "book", resourceType: "book",
resourceId: id, resourceId: id,
details: `Added comment to book`, details: `Added comment to book`,
@@ -169,8 +169,8 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
const comment = await commentService.updateComment(commentId, request.body.content); const comment = await commentService.updateComment(commentId, request.body.content);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_UPDATE, action: AuditAction.commentUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "book", resourceType: "book",
resourceId: id, resourceId: id,
details: `Updated comment ${commentId} on book`, details: `Updated comment ${commentId} on book`,
@@ -205,8 +205,8 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
await commentService.deleteComment(commentId); await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE, action: AuditAction.commentDelete,
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content,
resourceType: "book", resourceType: "book",
resourceId: id, resourceId: id,
details: `Deleted comment ${commentId} from book`, details: `Deleted comment ${commentId} from book`,
+12 -12
View File
@@ -40,8 +40,8 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
async (request) => { async (request) => {
const game = await gameService.createGame(request.body); const game = await gameService.createGame(request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "game", resourceType: "game",
resourceId: game.id, resourceId: game.id,
details: `Created game: ${game.title}`, details: `Created game: ${game.title}`,
@@ -66,8 +66,8 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
const game = await gameService.updateGame(id, request.body); const game = await gameService.updateGame(id, request.body);
if (game) { if (game) {
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "game", resourceType: "game",
resourceId: id, resourceId: id,
details: `Updated game: ${game.title}`, details: `Updated game: ${game.title}`,
@@ -88,8 +88,8 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
const { id } = request.params; const { id } = request.params;
await gameService.deleteGame(id); await gameService.deleteGame(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "game", resourceType: "game",
resourceId: id, resourceId: id,
details: `Deleted game with ID: ${id}`, details: `Deleted game with ID: ${id}`,
@@ -119,8 +119,8 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
const userId = request.user.id; const userId = request.user.id;
const comment = await commentService.createCommentForGame(id, userId, request.body); const comment = await commentService.createCommentForGame(id, userId, request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE, action: AuditAction.commentCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "game", resourceType: "game",
resourceId: id, resourceId: id,
details: `Added comment to game`, details: `Added comment to game`,
@@ -153,8 +153,8 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
const comment = await commentService.updateComment(commentId, request.body.content); const comment = await commentService.updateComment(commentId, request.body.content);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_UPDATE, action: AuditAction.commentUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "game", resourceType: "game",
resourceId: id, resourceId: id,
details: `Updated comment ${commentId} on game`, details: `Updated comment ${commentId} on game`,
@@ -187,8 +187,8 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
await commentService.deleteComment(commentId); await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE, action: AuditAction.commentDelete,
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content,
resourceType: "game", resourceType: "game",
resourceId: id, resourceId: id,
details: `Deleted comment ${commentId} from game`, details: `Deleted comment ${commentId} from game`,
+12 -12
View File
@@ -37,8 +37,8 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
async (request) => { async (request) => {
const manga = await mangaService.createManga(request.body); const manga = await mangaService.createManga(request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "manga", resourceType: "manga",
resourceId: manga.id, resourceId: manga.id,
details: `Created manga: ${manga.title}`, details: `Created manga: ${manga.title}`,
@@ -62,8 +62,8 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
const manga = await mangaService.updateManga(id, request.body); const manga = await mangaService.updateManga(id, request.body);
if (manga) { if (manga) {
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "manga", resourceType: "manga",
resourceId: id, resourceId: id,
details: `Updated manga: ${manga.title}`, details: `Updated manga: ${manga.title}`,
@@ -83,8 +83,8 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
const { id } = request.params; const { id } = request.params;
await mangaService.deleteManga(id); await mangaService.deleteManga(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "manga", resourceType: "manga",
resourceId: id, resourceId: id,
details: `Deleted manga with ID: ${id}`, details: `Deleted manga with ID: ${id}`,
@@ -112,8 +112,8 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
const userId = request.user.id; const userId = request.user.id;
const comment = await commentService.createCommentForManga(id, userId, request.body); const comment = await commentService.createCommentForManga(id, userId, request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE, action: AuditAction.commentCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "manga", resourceType: "manga",
resourceId: id, resourceId: id,
details: `Added comment to manga`, details: `Added comment to manga`,
@@ -145,8 +145,8 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
const comment = await commentService.updateComment(commentId, request.body.content); const comment = await commentService.updateComment(commentId, request.body.content);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_UPDATE, action: AuditAction.commentUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "manga", resourceType: "manga",
resourceId: id, resourceId: id,
details: `Updated comment ${commentId} on manga`, details: `Updated comment ${commentId} on manga`,
@@ -178,8 +178,8 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
await commentService.deleteComment(commentId); await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE, action: AuditAction.commentDelete,
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content,
resourceType: "manga", resourceType: "manga",
resourceId: id, resourceId: id,
details: `Deleted comment ${commentId} from manga`, details: `Deleted comment ${commentId} from manga`,
+12 -12
View File
@@ -46,8 +46,8 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
async (request) => { async (request) => {
const music = await musicService.createMusic(request.body); const music = await musicService.createMusic(request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "music", resourceType: "music",
resourceId: music.id, resourceId: music.id,
details: `Created music: ${music.title}`, details: `Created music: ${music.title}`,
@@ -74,8 +74,8 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
const music = await musicService.updateMusic(id, request.body); const music = await musicService.updateMusic(id, request.body);
if (music) { if (music) {
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "music", resourceType: "music",
resourceId: id, resourceId: id,
details: `Updated music: ${music.title}`, details: `Updated music: ${music.title}`,
@@ -98,8 +98,8 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
const { id } = request.params; const { id } = request.params;
await musicService.deleteMusic(id); await musicService.deleteMusic(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "music", resourceType: "music",
resourceId: id, resourceId: id,
details: `Deleted music with ID: ${id}`, details: `Deleted music with ID: ${id}`,
@@ -133,8 +133,8 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
const userId = request.user.id; const userId = request.user.id;
const comment = await commentService.createCommentForMusic(id, userId, request.body); const comment = await commentService.createCommentForMusic(id, userId, request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE, action: AuditAction.commentCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "music", resourceType: "music",
resourceId: id, resourceId: id,
details: `Added comment to music`, details: `Added comment to music`,
@@ -169,8 +169,8 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
const comment = await commentService.updateComment(commentId, request.body.content); const comment = await commentService.updateComment(commentId, request.body.content);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_UPDATE, action: AuditAction.commentUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "music", resourceType: "music",
resourceId: id, resourceId: id,
details: `Updated comment ${commentId} on music`, details: `Updated comment ${commentId} on music`,
@@ -205,8 +205,8 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
await commentService.deleteComment(commentId); await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE, action: AuditAction.commentDelete,
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content,
resourceType: "music", resourceType: "music",
resourceId: id, resourceId: id,
details: `Deleted comment ${commentId} from music`, details: `Deleted comment ${commentId} from music`,
+12 -12
View File
@@ -37,8 +37,8 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
async (request) => { async (request) => {
const show = await showService.createShow(request.body); const show = await showService.createShow(request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "show", resourceType: "show",
resourceId: show.id, resourceId: show.id,
details: `Created show: ${show.title}`, details: `Created show: ${show.title}`,
@@ -62,8 +62,8 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
const show = await showService.updateShow(id, request.body); const show = await showService.updateShow(id, request.body);
if (show) { if (show) {
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "show", resourceType: "show",
resourceId: id, resourceId: id,
details: `Updated show: ${show.title}`, details: `Updated show: ${show.title}`,
@@ -83,8 +83,8 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
const { id } = request.params; const { id } = request.params;
await showService.deleteShow(id); await showService.deleteShow(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "show", resourceType: "show",
resourceId: id, resourceId: id,
details: `Deleted show with ID: ${id}`, details: `Deleted show with ID: ${id}`,
@@ -112,8 +112,8 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
const userId = request.user.id; const userId = request.user.id;
const comment = await commentService.createCommentForShow(id, userId, request.body); const comment = await commentService.createCommentForShow(id, userId, request.body);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_CREATE, action: AuditAction.commentCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "show", resourceType: "show",
resourceId: id, resourceId: id,
details: `Added comment to show`, details: `Added comment to show`,
@@ -145,8 +145,8 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
const comment = await commentService.updateComment(commentId, request.body.content); const comment = await commentService.updateComment(commentId, request.body.content);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_UPDATE, action: AuditAction.commentUpdate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "show", resourceType: "show",
resourceId: id, resourceId: id,
details: `Updated comment ${commentId} on show`, details: `Updated comment ${commentId} on show`,
@@ -178,8 +178,8 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
await commentService.deleteComment(commentId); await commentService.deleteComment(commentId);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.COMMENT_DELETE, action: AuditAction.commentDelete,
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content,
resourceType: "show", resourceType: "show",
resourceId: id, resourceId: id,
details: `Deleted comment ${commentId} from show`, details: `Deleted comment ${commentId} from show`,
+10 -10
View File
@@ -85,8 +85,8 @@ export default async function (app: FastifyInstance): Promise<void> {
); );
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_CREATE, action: AuditAction.entryCreate,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: "Suggestion", resourceType: "Suggestion",
resourceId: suggestion.id, resourceId: suggestion.id,
details: `Created ${suggestion.entityType} suggestion: ${suggestion.title}`, details: `Created ${suggestion.entityType} suggestion: ${suggestion.title}`,
@@ -115,8 +115,8 @@ export default async function (app: FastifyInstance): Promise<void> {
const suggestion = await SuggestionService.acceptSuggestion(id); const suggestion = await SuggestionService.acceptSuggestion(id);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.ADMIN, category: AuditCategory.admin,
resourceType: "Suggestion", resourceType: "Suggestion",
resourceId: suggestion.id, resourceId: suggestion.id,
details: `Accepted ${suggestion.entityType} suggestion: ${suggestion.title}`, details: `Accepted ${suggestion.entityType} suggestion: ${suggestion.title}`,
@@ -146,8 +146,8 @@ export default async function (app: FastifyInstance): Promise<void> {
const suggestion = await SuggestionService.acceptSuggestionWithEdits(id, editedData); const suggestion = await SuggestionService.acceptSuggestionWithEdits(id, editedData);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.ADMIN, category: AuditCategory.admin,
resourceType: "Suggestion", resourceType: "Suggestion",
resourceId: suggestion.id, resourceId: suggestion.id,
details: `Accepted ${suggestion.entityType} suggestion with edits: ${suggestion.title}`, details: `Accepted ${suggestion.entityType} suggestion with edits: ${suggestion.title}`,
@@ -177,8 +177,8 @@ export default async function (app: FastifyInstance): Promise<void> {
const suggestion = await SuggestionService.declineSuggestion(id, reason); const suggestion = await SuggestionService.declineSuggestion(id, reason);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_UPDATE, action: AuditAction.entryUpdate,
category: AuditCategory.ADMIN, category: AuditCategory.admin,
resourceType: "Suggestion", resourceType: "Suggestion",
resourceId: suggestion.id, resourceId: suggestion.id,
details: `Declined ${suggestion.entityType} suggestion: ${suggestion.title}${reason ? ` (Reason: ${reason})` : ""}`, details: `Declined ${suggestion.entityType} suggestion: ${suggestion.title}${reason ? ` (Reason: ${reason})` : ""}`,
@@ -209,8 +209,8 @@ export default async function (app: FastifyInstance): Promise<void> {
const suggestion = await SuggestionService.deleteSuggestion(id, userId, isAdmin); const suggestion = await SuggestionService.deleteSuggestion(id, userId, isAdmin);
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.ENTRY_DELETE, action: AuditAction.entryDelete,
category: isAdmin ? AuditCategory.ADMIN : AuditCategory.CONTENT, category: isAdmin ? AuditCategory.admin : AuditCategory.content,
resourceType: "Suggestion", resourceType: "Suggestion",
resourceId: suggestion.id, resourceId: suggestion.id,
details: `Deleted ${suggestion.entityType} suggestion: ${suggestion.title}`, details: `Deleted ${suggestion.entityType} suggestion: ${suggestion.title}`,
+4 -4
View File
@@ -54,8 +54,8 @@ const usersRoutes: FastifyPluginAsync = async (app) => {
} }
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.USER_BAN, action: AuditAction.userBan,
category: AuditCategory.ADMIN, category: AuditCategory.admin,
targetUserId: id, targetUserId: id,
details: `Banned user: ${user.username}`, details: `Banned user: ${user.username}`,
}); });
@@ -78,8 +78,8 @@ const usersRoutes: FastifyPluginAsync = async (app) => {
} }
await AuditService.logFromRequest(request, { await AuditService.logFromRequest(request, {
action: AuditAction.USER_UNBAN, action: AuditAction.userUnban,
category: AuditCategory.ADMIN, category: AuditCategory.admin,
targetUserId: id, targetUserId: id,
details: `Unbanned user: ${user.username}`, details: `Unbanned user: ${user.username}`,
}); });
+1 -1
View File
@@ -36,7 +36,7 @@ export const AuditService = {
request: FastifyRequest, request: FastifyRequest,
data: Omit<AuditLogData, "userId"> data: Omit<AuditLogData, "userId">
) { ) {
const userId = (request.user as { id?: string } | undefined)?.id; const userId = ((request as any).user as { id?: string } | undefined)?.id;
return this.log( return this.log(
{ {
+4 -4
View File
@@ -32,8 +32,8 @@ export class LikeService {
}); });
await AuditService.logFromRequest(req, { await AuditService.logFromRequest(req, {
action: AuditAction.UNLIKE, action: AuditAction.unlike,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: entityType, resourceType: entityType,
resourceId: entityId, resourceId: entityId,
details: `Unliked ${entityType}` details: `Unliked ${entityType}`
@@ -52,8 +52,8 @@ export class LikeService {
}); });
await AuditService.logFromRequest(req, { await AuditService.logFromRequest(req, {
action: AuditAction.LIKE, action: AuditAction.like,
category: AuditCategory.CONTENT, category: AuditCategory.content,
resourceType: entityType, resourceType: entityType,
resourceId: entityId, resourceId: entityId,
details: `Liked ${entityType}` details: `Liked ${entityType}`
+15
View File
@@ -0,0 +1,15 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
// Set required environment variables for tests
process.env.JWT_SECRET = 'test-secret';
process.env.DISCORD_CLIENT_ID = 'test-client-id';
process.env.DISCORD_CLIENT_SECRET = 'test-client-secret';
process.env.DOMAIN = 'http://localhost:3000';
process.env.API_URL = 'http://localhost:3000/api';
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test';
process.env.BASE_URL = 'http://localhost:4200';
process.env.NODE_ENV = 'test';
@@ -206,8 +206,8 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
} }
@if (showDeclineModal()) { @if (showDeclineModal()) {
<div class="modal-overlay" (click)="closeDeclineModal()"> <div class="modal-overlay" (click)="closeDeclineModal()" (keyup.escape)="closeDeclineModal()" tabindex="0" role="button">
<div class="modal" (click)="$event.stopPropagation()"> <div class="modal" (click)="$event.stopPropagation()" (keyup)="$event.stopPropagation()" tabindex="-1">
<h3>Decline Suggestion</h3> <h3>Decline Suggestion</h3>
<p>Are you sure you want to decline "{{ decliningsuggestion()?.title }}"?</p> <p>Are you sure you want to decline "{{ decliningsuggestion()?.title }}"?</p>
<div class="form-group"> <div class="form-group">
@@ -229,8 +229,8 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
} }
@if (showEditModal()) { @if (showEditModal()) {
<div class="modal-overlay" (click)="closeEditModal()"> <div class="modal-overlay" (click)="closeEditModal()" (keyup.escape)="closeEditModal()" tabindex="0" role="button">
<div class="modal edit-modal" (click)="$event.stopPropagation()"> <div class="modal edit-modal" (click)="$event.stopPropagation()" (keyup)="$event.stopPropagation()" tabindex="-1">
<h3>Review & Edit Before Accepting</h3> <h3>Review & Edit Before Accepting</h3>
<p>Review and edit the details before adding to your collection.</p> <p>Review and edit the details before adding to your collection.</p>
@@ -105,8 +105,7 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
} }
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of newArt.tags; track tag; let i = $index) { @for (tag of newArt.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -123,8 +122,7 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of newArt.links; track link.url; let i = $index) { @for (link of newArt.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -280,8 +278,7 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
} }
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of editArt.tags; track tag; let i = $index) { @for (tag of editArt.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -298,8 +295,7 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of editArt.links; track link.url; let i = $index) { @for (link of editArt.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -401,6 +397,10 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
[alt]="art.description || art.title" [alt]="art.description || art.title"
class="art-image" class="art-image"
(click)="openLightbox(art)" (click)="openLightbox(art)"
(keyup.enter)="openLightbox(art)"
(keyup.space)="openLightbox(art)"
tabindex="0"
role="button"
> >
</div> </div>
@@ -536,8 +536,8 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
} }
@if (lightboxArt()) { @if (lightboxArt()) {
<div class="lightbox" (click)="closeLightbox()"> <div class="lightbox" (click)="closeLightbox()" (keyup.escape)="closeLightbox()" tabindex="0" role="button">
<div class="lightbox-content" (click)="$event.stopPropagation()"> <div class="lightbox-content" (click)="$event.stopPropagation()" (keyup)="$event.stopPropagation()" tabindex="-1">
<button class="lightbox-close" (click)="closeLightbox()">&times;</button> <button class="lightbox-close" (click)="closeLightbox()">&times;</button>
<img [src]="lightboxArt()!.imageUrl" [alt]="lightboxArt()!.description || lightboxArt()!.title"> <img [src]="lightboxArt()!.imageUrl" [alt]="lightboxArt()!.description || lightboxArt()!.title">
<div class="lightbox-info"> <div class="lightbox-info">
@@ -126,8 +126,7 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of newBook.tags; track tag; let i = $index) { @for (tag of newBook.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -144,8 +143,7 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of newBook.links; track link.url; let i = $index) { @for (link of newBook.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -269,8 +267,7 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of editBook.tags; track tag; let i = $index) { @for (tag of editBook.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -287,8 +284,7 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of editBook.links; track link.url; let i = $index) { @for (link of editBook.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -421,8 +417,7 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
@if (showFilters()) { @if (showFilters()) {
<div class="advanced-filters"> <div class="advanced-filters">
<div class="filter-group"> <div class="filter-group">
<label>Filter by Tags:</label> <div class="tags-filter" aria-label="Filter by Tags">
<div class="tags-filter">
@for (tag of allTags(); track tag) { @for (tag of allTags(); track tag) {
<label class="tag-checkbox"> <label class="tag-checkbox">
<input <input
@@ -114,8 +114,7 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of newGame.tags; track tag; let i = $index) { @for (tag of newGame.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -132,8 +131,7 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of newGame.links; track link.url; let i = $index) { @for (link of newGame.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -245,8 +243,7 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of editGame.tags; track tag; let i = $index) { @for (tag of editGame.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -263,8 +260,7 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of editGame.links; track link.url; let i = $index) { @for (link of editGame.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -115,8 +115,7 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of newManga.tags; track tag; let i = $index) { @for (tag of newManga.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -133,8 +132,7 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of newManga.links; track link.url; let i = $index) { @for (link of newManga.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -247,8 +245,7 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of editManga.tags; track tag; let i = $index) { @for (tag of editManga.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -265,8 +262,7 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of editManga.links; track link.url; let i = $index) { @for (link of editManga.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -124,8 +124,7 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of newMusic.tags; track tag; let i = $index) { @for (tag of newMusic.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -142,8 +141,7 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of newMusic.links; track link.url; let i = $index) { @for (link of newMusic.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -265,8 +263,7 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of editMusicData.tags; track tag; let i = $index) { @for (tag of editMusicData.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -283,8 +280,7 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of editMusicData.links; track link.url; let i = $index) { @for (link of editMusicData.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -113,8 +113,7 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of newShow.tags; track tag; let i = $index) { @for (tag of newShow.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -131,8 +130,7 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of newShow.links; track link.url; let i = $index) { @for (link of newShow.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -243,8 +241,7 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Tags</label> <div class="tags-input-container" aria-label="Tags">
<div class="tags-input-container">
@for (tag of editShow.tags; track tag; let i = $index) { @for (tag of editShow.tags; track tag; let i = $index) {
<span class="tag"> <span class="tag">
{{ tag }} {{ tag }}
@@ -261,8 +258,7 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" aria-label="External Links">
<label>External Links</label>
<div class="links-list"> <div class="links-list">
@for (link of editShow.links; track link.url; let i = $index) { @for (link of editShow.links; track link.url; let i = $index) {
<div class="link-item"> <div class="link-item">
@@ -19,14 +19,12 @@ import { environment } from '../../environments/environment';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
private router = inject(Router);
private http = inject(HttpClient);
private isRefreshing = false; private isRefreshing = false;
private refreshTokenSubject = new BehaviorSubject<boolean | null>(null); private refreshTokenSubject = new BehaviorSubject<boolean | null>(null);
constructor(
private router: Router,
private http: HttpClient
) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Clone the request to add withCredentials // Clone the request to add withCredentials
const authReq = request.clone({ const authReq = request.clone({
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Art, CreateArtDto, UpdateArtDto } from '@library/shared-types'; import { Art, CreateArtDto, UpdateArtDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Art, CreateArtDto, UpdateArtDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class ArtService { export class ArtService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getAllArt(): Observable<Art[]> { getAllArt(): Observable<Art[]> {
return this.api.get<Art[]>('/art'); return this.api.get<Art[]>('/art');
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable, signal } from '@angular/core'; import { Injectable, signal, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable, tap, catchError, switchMap, throwError, of } from 'rxjs'; import { Observable, tap, catchError, switchMap, throwError, of } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
@@ -16,16 +16,14 @@ import { HttpClient } from '@angular/common/http';
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService { export class AuthService {
private api = inject(ApiService);
private router = inject(Router);
private http = inject(HttpClient);
private currentUser = signal<User | null>(null); private currentUser = signal<User | null>(null);
public readonly user = this.currentUser.asReadonly(); public readonly user = this.currentUser.asReadonly();
private refreshing = false; private refreshing = false;
constructor(
private api: ApiService,
private router: Router,
private http: HttpClient
) {}
login(): void { login(): void {
// Redirect to API login endpoint // Redirect to API login endpoint
window.location.href = `${environment.apiUrl}/auth/login`; window.location.href = `${environment.apiUrl}/auth/login`;
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Book, CreateBookDto, UpdateBookDto } from '@library/shared-types'; import { Book, CreateBookDto, UpdateBookDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Book, CreateBookDto, UpdateBookDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class BooksService { export class BooksService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getAllBooks(): Observable<Book[]> { getAllBooks(): Observable<Book[]> {
return this.api.get<Book[]>('/books'); return this.api.get<Book[]>('/books');
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Comment, CreateCommentDto } from '@library/shared-types'; import { Comment, CreateCommentDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Comment, CreateCommentDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class CommentsService { export class CommentsService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getCommentsForGame(gameId: string): Observable<Comment[]> { getCommentsForGame(gameId: string): Observable<Comment[]> {
return this.api.get<Comment[]>(`/games/${gameId}/comments`); return this.api.get<Comment[]>(`/games/${gameId}/comments`);
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Game, CreateGameDto, UpdateGameDto } from '@library/shared-types'; import { Game, CreateGameDto, UpdateGameDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Game, CreateGameDto, UpdateGameDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class GamesService { export class GamesService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getAllGames(): Observable<Game[]> { getAllGames(): Observable<Game[]> {
return this.api.get<Game[]>('/games'); return this.api.get<Game[]>('/games');
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Manga, CreateMangaDto, UpdateMangaDto } from '@library/shared-types'; import { Manga, CreateMangaDto, UpdateMangaDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Manga, CreateMangaDto, UpdateMangaDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class MangaService { export class MangaService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getAllManga(): Observable<Manga[]> { getAllManga(): Observable<Manga[]> {
return this.api.get<Manga[]>('/manga'); return this.api.get<Manga[]>('/manga');
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Music, CreateMusicDto, UpdateMusicDto } from '@library/shared-types'; import { Music, CreateMusicDto, UpdateMusicDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Music, CreateMusicDto, UpdateMusicDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class MusicService { export class MusicService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getAllMusic(): Observable<Music[]> { getAllMusic(): Observable<Music[]> {
return this.api.get<Music[]>('/music'); return this.api.get<Music[]>('/music');
@@ -4,7 +4,7 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Show, CreateShowDto, UpdateShowDto } from '@library/shared-types'; import { Show, CreateShowDto, UpdateShowDto } from '@library/shared-types';
@@ -13,7 +13,8 @@ import { Show, CreateShowDto, UpdateShowDto } from '@library/shared-types';
providedIn: 'root' providedIn: 'root'
}) })
export class ShowsService { export class ShowsService {
constructor(private api: ApiService) {} private api = inject(ApiService);
getAllShows(): Observable<Show[]> { getAllShows(): Observable<Show[]> {
return this.api.get<Show[]>('/shows'); return this.api.get<Show[]>('/shows');
+11 -11
View File
@@ -1,17 +1,17 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
export * from "./lib/game.types"; export type * from "./lib/art.types";
export * from "./lib/book.types";
export * from "./lib/music.types";
export * from "./lib/art.types";
export * from "./lib/show.types";
export * from "./lib/manga.types";
export type * from "./lib/auth.types";
export * from "./lib/comment.types";
export * from "./lib/audit.types"; export * from "./lib/audit.types";
export type * from "./lib/auth.types";
export * from "./lib/book.types";
export type * from "./lib/comment.types";
export type * from "./lib/common.types";
export * from "./lib/game.types";
export type * from "./lib/like.types";
export * from "./lib/manga.types";
export * from "./lib/music.types";
export * from "./lib/show.types";
export * from "./lib/suggestion.types"; export * from "./lib/suggestion.types";
export * from "./lib/common.types";
export * from "./lib/like.types";
+22 -19
View File
@@ -1,30 +1,33 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan
*/ */
import { Link } from "./common.types"; import type { Link } from "./common.types";
export interface Art { interface Art {
id: string; id: string;
title: string; title: string;
artist: string; artist: string;
description?: string; description?: string;
imageUrl: string; imageUrl: string;
tags: string[]; tags: Array<string>;
links: Link[]; links: Array<Link>;
dateAdded: Date; dateAdded: Date;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateArtDto { interface CreateArtDto {
title: string; title: string;
artist: string; artist: string;
description?: string; description?: string;
imageUrl: string; imageUrl: string;
tags?: string[]; tags?: Array<string>;
links?: Link[]; links?: Array<Link>;
} }
export interface UpdateArtDto extends Partial<CreateArtDto> {} type UpdateArtDto = Partial<CreateArtDto>;
export type { Art, CreateArtDto, UpdateArtDto };
+59 -45
View File
@@ -1,58 +1,72 @@
export enum AuditAction { /**
LOGIN = "LOGIN", * @copyright NHCarrigan
LOGOUT = "LOGOUT", * @license Naomi's Public License
LOGIN_FAILED = "LOGIN_FAILED", * @author Naomi Carrigan
COMMENT_CREATE = "COMMENT_CREATE", */
COMMENT_UPDATE = "COMMENT_UPDATE",
COMMENT_DELETE = "COMMENT_DELETE", enum AuditAction {
ENTRY_CREATE = "ENTRY_CREATE", login = "LOGIN",
ENTRY_UPDATE = "ENTRY_UPDATE", logout = "LOGOUT",
ENTRY_DELETE = "ENTRY_DELETE", loginFailed = "LOGIN_FAILED",
LIKE = "LIKE", commentCreate = "COMMENT_CREATE",
UNLIKE = "UNLIKE", commentUpdate = "COMMENT_UPDATE",
USER_BAN = "USER_BAN", commentDelete = "COMMENT_DELETE",
USER_UNBAN = "USER_UNBAN", entryCreate = "ENTRY_CREATE",
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED", entryUpdate = "ENTRY_UPDATE",
CSRF_VALIDATION_FAILED = "CSRF_VALIDATION_FAILED", entryDelete = "ENTRY_DELETE",
UNAUTHORIZED_ACCESS = "UNAUTHORIZED_ACCESS", like = "LIKE",
unlike = "UNLIKE",
userBan = "USER_BAN",
userUnban = "USER_UNBAN",
rateLimitExceeded = "RATE_LIMIT_EXCEEDED",
csrfValidationFailed = "CSRF_VALIDATION_FAILED",
unauthorizedAccess = "UNAUTHORIZED_ACCESS",
} }
export enum AuditCategory { enum AuditCategory {
AUTH = "AUTH", auth = "AUTH",
CONTENT = "CONTENT", content = "CONTENT",
ADMIN = "ADMIN", admin = "ADMIN",
SECURITY = "SECURITY", security = "SECURITY",
} }
export interface AuditLogUser { interface AuditLogUser {
id: string; id: string;
username: string; username: string;
avatar?: string; avatar?: string;
} }
export interface AuditLog { interface AuditLog {
id: string; id: string;
action: AuditAction; action: AuditAction;
category: AuditCategory; category: AuditCategory;
userId?: string; userId?: string;
user?: AuditLogUser; user?: AuditLogUser;
targetUserId?: string; targetUserId?: string;
targetUser?: AuditLogUser; targetUser?: AuditLogUser;
resourceType?: string; resourceType?: string;
resourceId?: string; resourceId?: string;
details?: string; details?: string;
userAgent?: string; userAgent?: string;
success: boolean; success: boolean;
createdAt: Date; createdAt: Date;
} }
export interface AuditLogFilters { interface AuditLogFilters {
action?: AuditAction; action?: AuditAction;
category?: AuditCategory; category?: AuditCategory;
userId?: string; userId?: string;
success?: boolean; success?: boolean;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
page?: number; page?: number;
limit?: number; limit?: number;
} }
export {
AuditAction,
AuditCategory,
type AuditLog,
type AuditLogFilters,
type AuditLogUser,
};
+22 -19
View File
@@ -4,33 +4,36 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
export interface User { interface User {
id: string; id: string;
email: string; email: string;
username: string; username: string;
avatar?: string; avatar?: string;
discordId: string; discordId: string;
isAdmin: boolean; isAdmin: boolean;
isBanned: boolean; isBanned: boolean;
inDiscord: boolean; inDiscord: boolean;
isVip: boolean; isVip: boolean;
isMod: boolean; isMod: boolean;
isStaff: boolean; isStaff: boolean;
} }
export interface JwtPayload { interface JwtPayload {
/** /**
* User id. * User id.
*/ */
sub: string; sub: string;
email: string; email: string;
username: string; username: string;
isAdmin: boolean; isAdmin: boolean;
iat?: number; iat?: number;
exp?: number; exp?: number;
} }
export interface AuthResponse { interface AuthResponse {
accessToken: string; accessToken: string;
user: User; user: User;
} }
export type { AuthResponse, JwtPayload, User };
+14 -12
View File
@@ -1,18 +1,18 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @copyright 2026 NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan
*/ */
export enum BookStatus { import type { Link } from "./common.types";
enum BookStatus {
reading = "READING", reading = "READING",
finished = "FINISHED", finished = "FINISHED",
toRead = "TO_READ", toRead = "TO_READ",
} }
import { Link } from "./common.types"; interface Book {
export interface Book {
id: string; id: string;
title: string; title: string;
author: string; author: string;
@@ -23,13 +23,13 @@ export interface Book {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags: string[]; tags: Array<string>;
links: Link[]; links: Array<Link>;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateBookDto { interface CreateBookDto {
title: string; title: string;
author: string; author: string;
isbn?: string; isbn?: string;
@@ -37,10 +37,12 @@ export interface CreateBookDto {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags?: string[]; tags?: Array<string>;
links?: Link[]; links?: Array<Link>;
} }
export interface UpdateBookDto extends Partial<CreateBookDto> { interface UpdateBookDto extends Partial<CreateBookDto> {
dateFinished?: Date; dateFinished?: Date;
} }
export { type Book, BookStatus, type CreateBookDto, type UpdateBookDto };
+23 -21
View File
@@ -4,32 +4,34 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
export interface CommentUser { interface CommentUser {
id: string; id: string;
username: string; username: string;
avatar?: string; avatar?: string;
inDiscord?: boolean; inDiscord?: boolean;
isVip?: boolean; isVip?: boolean;
isMod?: boolean; isMod?: boolean;
isStaff?: boolean; isStaff?: boolean;
} }
export interface Comment { interface Comment {
id: string; id: string;
content: string; content: string;
rawContent?: string; rawContent?: string;
userId: string; userId: string;
user: CommentUser; user: CommentUser;
gameId?: string; gameId?: string;
bookId?: string; bookId?: string;
musicId?: string; musicId?: string;
artId?: string; artId?: string;
showId?: string; showId?: string;
mangaId?: string; mangaId?: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateCommentDto { interface CreateCommentDto {
content: string; content: string;
} }
export type { Comment, CommentUser, CreateCommentDto };
+7 -1
View File
@@ -1,4 +1,10 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Link { export interface Link {
title: string; title: string;
url: string; url: string;
} }
+14 -12
View File
@@ -1,18 +1,18 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @copyright 2026 NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan
*/ */
export enum GameStatus { import type { Link } from "./common.types";
enum GameStatus {
playing = "PLAYING", playing = "PLAYING",
completed = "COMPLETED", completed = "COMPLETED",
backlog = "BACKLOG", backlog = "BACKLOG",
} }
import { Link } from "./common.types"; interface Game {
export interface Game {
id: string; id: string;
title: string; title: string;
platform?: string; platform?: string;
@@ -22,23 +22,25 @@ export interface Game {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags: string[]; tags: Array<string>;
links: Link[]; links: Array<Link>;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateGameDto { interface CreateGameDto {
title: string; title: string;
platform?: string; platform?: string;
status: GameStatus; status: GameStatus;
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags?: string[]; tags?: Array<string>;
links?: Link[]; links?: Array<Link>;
} }
export interface UpdateGameDto extends Partial<CreateGameDto> { interface UpdateGameDto extends Partial<CreateGameDto> {
dateCompleted?: Date; dateCompleted?: Date;
} }
export { type CreateGameDto, type Game, GameStatus, type UpdateGameDto };
+21 -15
View File
@@ -1,32 +1,38 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
export interface Like { interface Like {
id: string; id: string;
userId: string; userId: string;
entityType: 'book' | 'game' | 'show' | 'manga' | 'music' | 'art'; entityType: "book" | "game" | "show" | "manga" | "music" | "art";
entityId: string; entityId: string;
createdAt: Date; createdAt: Date;
} }
export type CreateLikeDto = Pick<Like, 'entityType' | 'entityId'>; type CreateLikeDto = Pick<Like, "entityType" | "entityId">;
export interface LikeCountDto { interface LikeCountDto {
entityId: string; entityId: string;
entityType: string; entityType: string;
count: number; count: number;
} }
export interface LikedItemDto { interface LikedItemDto {
like: Like; like: Like;
item: any; // This will be the actual entity (Book, Game, etc.)
/**
* This will be the actual entity (Book, Game, etc.).
*/
item: unknown;
} }
// Response types // Response types
export interface LikeResponse { interface LikeResponse {
liked: boolean; liked: boolean;
count: number; count: number;
} }
export type { CreateLikeDto, Like, LikeCountDto, LikedItemDto, LikeResponse };
+14 -11
View File
@@ -1,18 +1,18 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
export enum MangaStatus { import type { Link } from "./common.types";
enum MangaStatus {
reading = "READING", reading = "READING",
completed = "COMPLETED", completed = "COMPLETED",
wantToRead = "WANT_TO_READ", wantToRead = "WANT_TO_READ",
} }
import { Link } from "./common.types"; interface Manga {
export interface Manga {
id: string; id: string;
title: string; title: string;
author: string; author: string;
@@ -22,23 +22,26 @@ export interface Manga {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags: string[]; tags: Array<string>;
links: Link[]; links: Array<Link>;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateMangaDto { interface CreateMangaDto {
title: string; title: string;
author: string; author: string;
status: MangaStatus; status: MangaStatus;
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags?: string[]; tags?: Array<string>;
links?: Link[]; links?: Array<Link>;
} }
export interface UpdateMangaDto extends Partial<CreateMangaDto> { interface UpdateMangaDto extends Partial<CreateMangaDto> {
dateCompleted?: Date; dateCompleted?: Date;
} }
export { MangaStatus };
export type { CreateMangaDto, Manga, UpdateMangaDto };
+16 -13
View File
@@ -1,24 +1,24 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @copyright 2026 NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan
*/ */
export enum MusicType { import type { Link } from "./common.types";
enum MusicType {
album = "ALBUM", album = "ALBUM",
single = "SINGLE", single = "SINGLE",
ep = "EP", ep = "EP",
} }
export enum MusicStatus { enum MusicStatus {
listening = "LISTENING", listening = "LISTENING",
completed = "COMPLETED", completed = "COMPLETED",
wantToListen = "WANT_TO_LISTEN", wantToListen = "WANT_TO_LISTEN",
} }
import { Link } from "./common.types"; interface Music {
export interface Music {
id: string; id: string;
title: string; title: string;
artist: string; artist: string;
@@ -29,13 +29,13 @@ export interface Music {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverArt?: string; coverArt?: string;
tags: string[]; tags: Array<string>;
links: Link[]; links: Array<Link>;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateMusicDto { interface CreateMusicDto {
title: string; title: string;
artist: string; artist: string;
type: MusicType; type: MusicType;
@@ -43,10 +43,13 @@ export interface CreateMusicDto {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverArt?: string; coverArt?: string;
tags?: string[]; tags?: Array<string>;
links?: Link[]; links?: Array<Link>;
} }
export interface UpdateMusicDto extends Partial<CreateMusicDto> { interface UpdateMusicDto extends Partial<CreateMusicDto> {
dateCompleted?: Date; dateCompleted?: Date;
} }
export { MusicStatus, MusicType };
export type { CreateMusicDto, Music, UpdateMusicDto };
+15 -12
View File
@@ -1,25 +1,25 @@
/** /**
* @copyright 2026 NHCarrigan * @copyright NHCarrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
export enum ShowType { import type { Link } from "./common.types";
enum ShowType {
tvSeries = "TV_SERIES", tvSeries = "TV_SERIES",
anime = "ANIME", anime = "ANIME",
film = "FILM", film = "FILM",
documentary = "DOCUMENTARY", documentary = "DOCUMENTARY",
} }
export enum ShowStatus { enum ShowStatus {
watching = "WATCHING", watching = "WATCHING",
completed = "COMPLETED", completed = "COMPLETED",
wantToWatch = "WANT_TO_WATCH", wantToWatch = "WANT_TO_WATCH",
} }
import { Link } from "./common.types"; interface Show {
export interface Show {
id: string; id: string;
title: string; title: string;
type: ShowType; type: ShowType;
@@ -29,23 +29,26 @@ export interface Show {
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags: string[]; tags: Array<string>;
links: Link[]; links: Array<Link>;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateShowDto { interface CreateShowDto {
title: string; title: string;
type: ShowType; type: ShowType;
status: ShowStatus; status: ShowStatus;
rating?: number; rating?: number;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
tags?: string[]; tags?: Array<string>;
links?: Link[]; links?: Array<Link>;
} }
export interface UpdateShowDto extends Partial<CreateShowDto> { interface UpdateShowDto extends Partial<CreateShowDto> {
dateCompleted?: Date; dateCompleted?: Date;
} }
export { ShowStatus, ShowType };
export type { CreateShowDto, Show, UpdateShowDto };
+106 -85
View File
@@ -1,104 +1,110 @@
import type { CreateGameDto } from "./game.types"; /**
import type { CreateBookDto } from "./book.types"; * @copyright NHCarrigan
import type { CreateMusicDto } from "./music.types"; * @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { CreateArtDto } from "./art.types"; import type { CreateArtDto } from "./art.types";
import type { CreateShowDto } from "./show.types"; import type { CreateBookDto } from "./book.types";
import type { CreateGameDto } from "./game.types";
import type { CreateMangaDto } from "./manga.types"; import type { CreateMangaDto } from "./manga.types";
import type { CreateMusicDto } from "./music.types";
import type { CreateShowDto } from "./show.types";
export enum SuggestionEntity { enum SuggestionEntity {
GAME = "GAME", game = "GAME",
BOOK = "BOOK", book = "BOOK",
MUSIC = "MUSIC", music = "MUSIC",
ART = "ART", art = "ART",
SHOW = "SHOW", show = "SHOW",
MANGA = "MANGA", manga = "MANGA",
} }
export enum SuggestionStatus { enum SuggestionStatus {
UNREVIEWED = "UNREVIEWED", unreviewed = "UNREVIEWED",
ACCEPTED = "ACCEPTED", accepted = "ACCEPTED",
DECLINED = "DECLINED", declined = "DECLINED",
} }
export interface SuggestionUser { interface SuggestionUser {
id: string; id: string;
username: string; username: string;
avatar?: string; avatar?: string;
inDiscord: boolean; inDiscord: boolean;
isVip: boolean; isVip: boolean;
isMod: boolean; isMod: boolean;
isStaff: boolean; isStaff: boolean;
} }
export interface Suggestion { interface Suggestion {
id: string; id: string;
userId: string; userId: string;
user: SuggestionUser; user: SuggestionUser;
entityType: SuggestionEntity; entityType: SuggestionEntity;
status: SuggestionStatus; status: SuggestionStatus;
declineReason?: string; declineReason?: string;
title: string; title: string;
gameData?: Omit<CreateGameDto, "rating">; gameData?: Omit<CreateGameDto, "rating">;
bookData?: Omit<CreateBookDto, "rating">; bookData?: Omit<CreateBookDto, "rating">;
musicData?: Omit<CreateMusicDto, "rating">; musicData?: Omit<CreateMusicDto, "rating">;
artData?: CreateArtDto; artData?: CreateArtDto;
showData?: Omit<CreateShowDto, "rating">; showData?: Omit<CreateShowDto, "rating">;
mangaData?: Omit<CreateMangaDto, "rating">; mangaData?: Omit<CreateMangaDto, "rating">;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface CreateGameSuggestionDto { interface CreateGameSuggestionDto {
entityType: SuggestionEntity.GAME; entityType: SuggestionEntity.game;
title: string; title: string;
platform?: string; platform?: string;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
} }
export interface CreateBookSuggestionDto { interface CreateBookSuggestionDto {
entityType: SuggestionEntity.BOOK; entityType: SuggestionEntity.book;
title: string; title: string;
author: string; author: string;
isbn?: string; isbn?: string;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
} }
export interface CreateMusicSuggestionDto { interface CreateMusicSuggestionDto {
entityType: SuggestionEntity.MUSIC; entityType: SuggestionEntity.music;
title: string; title: string;
artist: string; artist: string;
type: string; type: string;
notes?: string; notes?: string;
coverArt?: string; coverArt?: string;
} }
export interface CreateArtSuggestionDto { interface CreateArtSuggestionDto {
entityType: SuggestionEntity.ART; entityType: SuggestionEntity.art;
title: string; title: string;
artist: string; artist: string;
description?: string; description?: string;
imageUrl: string; imageUrl: string;
} }
export interface CreateShowSuggestionDto { interface CreateShowSuggestionDto {
entityType: SuggestionEntity.SHOW; entityType: SuggestionEntity.show;
title: string; title: string;
type: string; type: string;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
} }
export interface CreateMangaSuggestionDto { interface CreateMangaSuggestionDto {
entityType: SuggestionEntity.MANGA; entityType: SuggestionEntity.manga;
title: string; title: string;
author: string; author: string;
notes?: string; notes?: string;
coverImage?: string; coverImage?: string;
} }
export type CreateSuggestionDto = type CreateSuggestionDto =
| CreateGameSuggestionDto | CreateGameSuggestionDto
| CreateBookSuggestionDto | CreateBookSuggestionDto
| CreateMusicSuggestionDto | CreateMusicSuggestionDto
@@ -106,22 +112,37 @@ export type CreateSuggestionDto =
| CreateShowSuggestionDto | CreateShowSuggestionDto
| CreateMangaSuggestionDto; | CreateMangaSuggestionDto;
export interface DeclineSuggestionDto { interface DeclineSuggestionDto {
reason?: string; reason?: string;
} }
export interface AcceptWithEditsDto { interface AcceptWithEditsDto {
title?: string; title?: string;
platform?: string; platform?: string;
author?: string; author?: string;
artist?: string; artist?: string;
isbn?: string; isbn?: string;
type?: string; type?: string;
notes?: string; notes?: string;
description?: string; description?: string;
coverImage?: string; coverImage?: string;
coverArt?: string; coverArt?: string;
imageUrl?: string; imageUrl?: string;
tags?: string[]; tags?: Array<string>;
links?: Array<{ label: string; url: string }>; links?: Array<{ label: string; url: string }>;
} }
export { SuggestionEntity, SuggestionStatus };
export type {
AcceptWithEditsDto,
CreateArtSuggestionDto,
CreateBookSuggestionDto,
CreateGameSuggestionDto,
CreateMangaSuggestionDto,
CreateMusicSuggestionDto,
CreateShowSuggestionDto,
CreateSuggestionDto,
DeclineSuggestionDto,
Suggestion,
SuggestionUser,
};