generated from nhcarrigan/template
feat: ability to edit and delete comments
This commit is contained in:
@@ -144,20 +144,69 @@ const artRoutes: FastifyPluginAsync = async (app) => {
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete comment (admin only).
|
||||
* Update comment (owner or admin).
|
||||
*/
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
|
||||
app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate, adminGuard],
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request) => {
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "art", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only edit your own comments" });
|
||||
}
|
||||
|
||||
const comment = await commentService.updateComment(commentId, request.body.content);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_UPDATE,
|
||||
category: AuditCategory.CONTENT,
|
||||
resourceType: "art",
|
||||
resourceId: id,
|
||||
details: `Updated comment ${commentId} on art`,
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete comment (owner or admin).
|
||||
*/
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "art", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only delete your own comments" });
|
||||
}
|
||||
|
||||
await commentService.deleteComment(commentId);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_DELETE,
|
||||
category: AuditCategory.ADMIN,
|
||||
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT,
|
||||
resourceType: "art",
|
||||
resourceId: id,
|
||||
details: `Deleted comment ${commentId} from art`,
|
||||
|
||||
@@ -144,20 +144,69 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete comment (admin only).
|
||||
* Update comment (owner or admin).
|
||||
*/
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
|
||||
app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate, adminGuard],
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request) => {
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "book", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only edit your own comments" });
|
||||
}
|
||||
|
||||
const comment = await commentService.updateComment(commentId, request.body.content);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_UPDATE,
|
||||
category: AuditCategory.CONTENT,
|
||||
resourceType: "book",
|
||||
resourceId: id,
|
||||
details: `Updated comment ${commentId} on book`,
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete comment (owner or admin).
|
||||
*/
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "book", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only delete your own comments" });
|
||||
}
|
||||
|
||||
await commentService.deleteComment(commentId);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_DELETE,
|
||||
category: AuditCategory.ADMIN,
|
||||
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT,
|
||||
resourceType: "book",
|
||||
resourceId: id,
|
||||
details: `Deleted comment ${commentId} from book`,
|
||||
|
||||
@@ -129,19 +129,66 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
);
|
||||
|
||||
// Delete comment (admin only)
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
|
||||
// Update comment (owner or admin)
|
||||
app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate, adminGuard],
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request) => {
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "game", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only edit your own comments" });
|
||||
}
|
||||
|
||||
const comment = await commentService.updateComment(commentId, request.body.content);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_UPDATE,
|
||||
category: AuditCategory.CONTENT,
|
||||
resourceType: "game",
|
||||
resourceId: id,
|
||||
details: `Updated comment ${commentId} on game`,
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
);
|
||||
|
||||
// Delete comment (owner or admin)
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "game", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only delete your own comments" });
|
||||
}
|
||||
|
||||
await commentService.deleteComment(commentId);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_DELETE,
|
||||
category: AuditCategory.ADMIN,
|
||||
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT,
|
||||
resourceType: "game",
|
||||
resourceId: id,
|
||||
details: `Deleted comment ${commentId} from game`,
|
||||
|
||||
@@ -122,18 +122,64 @@ const mangaRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
|
||||
app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate, adminGuard],
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request) => {
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "manga", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only edit your own comments" });
|
||||
}
|
||||
|
||||
const comment = await commentService.updateComment(commentId, request.body.content);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_UPDATE,
|
||||
category: AuditCategory.CONTENT,
|
||||
resourceType: "manga",
|
||||
resourceId: id,
|
||||
details: `Updated comment ${commentId} on manga`,
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "manga", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only delete your own comments" });
|
||||
}
|
||||
|
||||
await commentService.deleteComment(commentId);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_DELETE,
|
||||
category: AuditCategory.ADMIN,
|
||||
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT,
|
||||
resourceType: "manga",
|
||||
resourceId: id,
|
||||
details: `Deleted comment ${commentId} from manga`,
|
||||
|
||||
@@ -144,20 +144,69 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete comment (admin only).
|
||||
* Update comment (owner or admin).
|
||||
*/
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
|
||||
app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate, adminGuard],
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request) => {
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "music", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only edit your own comments" });
|
||||
}
|
||||
|
||||
const comment = await commentService.updateComment(commentId, request.body.content);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_UPDATE,
|
||||
category: AuditCategory.CONTENT,
|
||||
resourceType: "music",
|
||||
resourceId: id,
|
||||
details: `Updated comment ${commentId} on music`,
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete comment (owner or admin).
|
||||
*/
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "music", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only delete your own comments" });
|
||||
}
|
||||
|
||||
await commentService.deleteComment(commentId);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_DELETE,
|
||||
category: AuditCategory.ADMIN,
|
||||
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT,
|
||||
resourceType: "music",
|
||||
resourceId: id,
|
||||
details: `Deleted comment ${commentId} from music`,
|
||||
|
||||
@@ -122,18 +122,64 @@ const showsRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
|
||||
app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate, adminGuard],
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request) => {
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "show", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only edit your own comments" });
|
||||
}
|
||||
|
||||
const comment = await commentService.updateComment(commentId, request.body.content);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_UPDATE,
|
||||
category: AuditCategory.CONTENT,
|
||||
resourceType: "show",
|
||||
resourceId: id,
|
||||
details: `Updated comment ${commentId} on show`,
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>(
|
||||
"/:id/comments/:commentId",
|
||||
{
|
||||
preValidation: [app.authenticate],
|
||||
preHandler: [app.csrfProtection],
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id, commentId } = request.params;
|
||||
const userId = request.user.id;
|
||||
const isAdmin = request.user.isAdmin;
|
||||
|
||||
const verification = await commentService.verifyCommentOwnership(commentId, "show", id);
|
||||
|
||||
if (!verification.exists) {
|
||||
return reply.code(404).send({ error: "Comment not found" });
|
||||
}
|
||||
|
||||
if (verification.comment?.userId !== userId && !isAdmin) {
|
||||
return reply.code(403).send({ error: "You can only delete your own comments" });
|
||||
}
|
||||
|
||||
await commentService.deleteComment(commentId);
|
||||
await AuditService.logFromRequest(request, {
|
||||
action: AuditAction.COMMENT_DELETE,
|
||||
category: AuditCategory.ADMIN,
|
||||
category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.ADMIN : AuditCategory.CONTENT,
|
||||
resourceType: "show",
|
||||
resourceId: id,
|
||||
details: `Deleted comment ${commentId} from show`,
|
||||
|
||||
@@ -54,6 +54,7 @@ export class CommentService {
|
||||
return {
|
||||
id: comment.id,
|
||||
content: comment.content,
|
||||
rawContent: comment.rawContent || undefined,
|
||||
userId: comment.userId,
|
||||
user: {
|
||||
id: comment.user.id,
|
||||
@@ -107,6 +108,7 @@ export class CommentService {
|
||||
const comment = await this.prisma.comment.create({
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: data.content,
|
||||
userId,
|
||||
gameId,
|
||||
},
|
||||
@@ -124,6 +126,7 @@ export class CommentService {
|
||||
const comment = await this.prisma.comment.create({
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: data.content,
|
||||
userId,
|
||||
bookId,
|
||||
},
|
||||
@@ -141,6 +144,7 @@ export class CommentService {
|
||||
const comment = await this.prisma.comment.create({
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: data.content,
|
||||
userId,
|
||||
musicId,
|
||||
},
|
||||
@@ -167,6 +171,7 @@ export class CommentService {
|
||||
const comment = await this.prisma.comment.create({
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: data.content,
|
||||
userId,
|
||||
artId,
|
||||
},
|
||||
@@ -193,6 +198,7 @@ export class CommentService {
|
||||
const comment = await this.prisma.comment.create({
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: data.content,
|
||||
userId,
|
||||
showId,
|
||||
},
|
||||
@@ -219,6 +225,7 @@ export class CommentService {
|
||||
const comment = await this.prisma.comment.create({
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: data.content,
|
||||
userId,
|
||||
mangaId,
|
||||
},
|
||||
@@ -227,9 +234,57 @@ export class CommentService {
|
||||
return this.mapComment(comment);
|
||||
}
|
||||
|
||||
async getCommentById(commentId: string) {
|
||||
return this.prisma.comment.findUnique({
|
||||
where: { id: commentId },
|
||||
include: { user: true },
|
||||
});
|
||||
}
|
||||
|
||||
async updateComment(
|
||||
commentId: string,
|
||||
content: string
|
||||
): Promise<Comment> {
|
||||
const sanitizedContent = this.sanitizeMarkdown(content);
|
||||
const comment = await this.prisma.comment.update({
|
||||
where: { id: commentId },
|
||||
data: {
|
||||
content: sanitizedContent,
|
||||
rawContent: content,
|
||||
},
|
||||
include: { user: true },
|
||||
});
|
||||
return this.mapComment(comment);
|
||||
}
|
||||
|
||||
async deleteComment(commentId: string): Promise<void> {
|
||||
await this.prisma.comment.delete({
|
||||
where: { id: commentId },
|
||||
});
|
||||
}
|
||||
|
||||
async verifyCommentOwnership(
|
||||
commentId: string,
|
||||
resourceType: "game" | "book" | "music" | "art" | "show" | "manga",
|
||||
resourceId: string
|
||||
): Promise<{ exists: boolean; comment?: { userId: string } }> {
|
||||
const fieldMap = {
|
||||
game: "gameId",
|
||||
book: "bookId",
|
||||
music: "musicId",
|
||||
art: "artId",
|
||||
show: "showId",
|
||||
manga: "mangaId",
|
||||
};
|
||||
|
||||
const comment = await this.prisma.comment.findFirst({
|
||||
where: {
|
||||
id: commentId,
|
||||
[fieldMap[resourceType]]: resourceId,
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
return comment ? { exists: true, comment } : { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user