generated from nhcarrigan/template
feat: ability to edit and delete comments
This commit is contained in:
@@ -161,6 +161,7 @@ model User {
|
|||||||
model Comment {
|
model Comment {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
content String
|
content String
|
||||||
|
rawContent String?
|
||||||
userId String @db.ObjectId
|
userId String @db.ObjectId
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
gameId String? @db.ObjectId
|
gameId String? @db.ObjectId
|
||||||
@@ -198,6 +199,7 @@ enum AuditAction {
|
|||||||
LOGOUT
|
LOGOUT
|
||||||
LOGIN_FAILED
|
LOGIN_FAILED
|
||||||
COMMENT_CREATE
|
COMMENT_CREATE
|
||||||
|
COMMENT_UPDATE
|
||||||
COMMENT_DELETE
|
COMMENT_DELETE
|
||||||
ENTRY_CREATE
|
ENTRY_CREATE
|
||||||
ENTRY_UPDATE
|
ENTRY_UPDATE
|
||||||
|
|||||||
@@ -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",
|
"/:id/comments/:commentId",
|
||||||
{
|
{
|
||||||
preValidation: [app.authenticate, adminGuard],
|
preValidation: [app.authenticate],
|
||||||
preHandler: [app.csrfProtection],
|
preHandler: [app.csrfProtection],
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request, reply) => {
|
||||||
const { id, commentId } = request.params;
|
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 commentService.deleteComment(commentId);
|
||||||
await AuditService.logFromRequest(request, {
|
await AuditService.logFromRequest(request, {
|
||||||
action: AuditAction.COMMENT_DELETE,
|
action: AuditAction.COMMENT_DELETE,
|
||||||
category: AuditCategory.ADMIN,
|
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`,
|
||||||
|
|||||||
@@ -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",
|
"/:id/comments/:commentId",
|
||||||
{
|
{
|
||||||
preValidation: [app.authenticate, adminGuard],
|
preValidation: [app.authenticate],
|
||||||
preHandler: [app.csrfProtection],
|
preHandler: [app.csrfProtection],
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request, reply) => {
|
||||||
const { id, commentId } = request.params;
|
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 commentService.deleteComment(commentId);
|
||||||
await AuditService.logFromRequest(request, {
|
await AuditService.logFromRequest(request, {
|
||||||
action: AuditAction.COMMENT_DELETE,
|
action: AuditAction.COMMENT_DELETE,
|
||||||
category: AuditCategory.ADMIN,
|
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`,
|
||||||
|
|||||||
@@ -129,19 +129,66 @@ const gamesRoutes: 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",
|
"/:id/comments/:commentId",
|
||||||
{
|
{
|
||||||
preValidation: [app.authenticate, adminGuard],
|
preValidation: [app.authenticate],
|
||||||
preHandler: [app.csrfProtection],
|
preHandler: [app.csrfProtection],
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request, reply) => {
|
||||||
const { id, commentId } = request.params;
|
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 commentService.deleteComment(commentId);
|
||||||
await AuditService.logFromRequest(request, {
|
await AuditService.logFromRequest(request, {
|
||||||
action: AuditAction.COMMENT_DELETE,
|
action: AuditAction.COMMENT_DELETE,
|
||||||
category: AuditCategory.ADMIN,
|
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`,
|
||||||
|
|||||||
@@ -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",
|
"/:id/comments/:commentId",
|
||||||
{
|
{
|
||||||
preValidation: [app.authenticate, adminGuard],
|
preValidation: [app.authenticate],
|
||||||
preHandler: [app.csrfProtection],
|
preHandler: [app.csrfProtection],
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request, reply) => {
|
||||||
const { id, commentId } = request.params;
|
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 commentService.deleteComment(commentId);
|
||||||
await AuditService.logFromRequest(request, {
|
await AuditService.logFromRequest(request, {
|
||||||
action: AuditAction.COMMENT_DELETE,
|
action: AuditAction.COMMENT_DELETE,
|
||||||
category: AuditCategory.ADMIN,
|
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`,
|
||||||
|
|||||||
@@ -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",
|
"/:id/comments/:commentId",
|
||||||
{
|
{
|
||||||
preValidation: [app.authenticate, adminGuard],
|
preValidation: [app.authenticate],
|
||||||
preHandler: [app.csrfProtection],
|
preHandler: [app.csrfProtection],
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request, reply) => {
|
||||||
const { id, commentId } = request.params;
|
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 commentService.deleteComment(commentId);
|
||||||
await AuditService.logFromRequest(request, {
|
await AuditService.logFromRequest(request, {
|
||||||
action: AuditAction.COMMENT_DELETE,
|
action: AuditAction.COMMENT_DELETE,
|
||||||
category: AuditCategory.ADMIN,
|
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`,
|
||||||
|
|||||||
@@ -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",
|
"/:id/comments/:commentId",
|
||||||
{
|
{
|
||||||
preValidation: [app.authenticate, adminGuard],
|
preValidation: [app.authenticate],
|
||||||
preHandler: [app.csrfProtection],
|
preHandler: [app.csrfProtection],
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request, reply) => {
|
||||||
const { id, commentId } = request.params;
|
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 commentService.deleteComment(commentId);
|
||||||
await AuditService.logFromRequest(request, {
|
await AuditService.logFromRequest(request, {
|
||||||
action: AuditAction.COMMENT_DELETE,
|
action: AuditAction.COMMENT_DELETE,
|
||||||
category: AuditCategory.ADMIN,
|
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`,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export class CommentService {
|
|||||||
return {
|
return {
|
||||||
id: comment.id,
|
id: comment.id,
|
||||||
content: comment.content,
|
content: comment.content,
|
||||||
|
rawContent: comment.rawContent || undefined,
|
||||||
userId: comment.userId,
|
userId: comment.userId,
|
||||||
user: {
|
user: {
|
||||||
id: comment.user.id,
|
id: comment.user.id,
|
||||||
@@ -107,6 +108,7 @@ export class CommentService {
|
|||||||
const comment = await this.prisma.comment.create({
|
const comment = await this.prisma.comment.create({
|
||||||
data: {
|
data: {
|
||||||
content: sanitizedContent,
|
content: sanitizedContent,
|
||||||
|
rawContent: data.content,
|
||||||
userId,
|
userId,
|
||||||
gameId,
|
gameId,
|
||||||
},
|
},
|
||||||
@@ -124,6 +126,7 @@ export class CommentService {
|
|||||||
const comment = await this.prisma.comment.create({
|
const comment = await this.prisma.comment.create({
|
||||||
data: {
|
data: {
|
||||||
content: sanitizedContent,
|
content: sanitizedContent,
|
||||||
|
rawContent: data.content,
|
||||||
userId,
|
userId,
|
||||||
bookId,
|
bookId,
|
||||||
},
|
},
|
||||||
@@ -141,6 +144,7 @@ export class CommentService {
|
|||||||
const comment = await this.prisma.comment.create({
|
const comment = await this.prisma.comment.create({
|
||||||
data: {
|
data: {
|
||||||
content: sanitizedContent,
|
content: sanitizedContent,
|
||||||
|
rawContent: data.content,
|
||||||
userId,
|
userId,
|
||||||
musicId,
|
musicId,
|
||||||
},
|
},
|
||||||
@@ -167,6 +171,7 @@ export class CommentService {
|
|||||||
const comment = await this.prisma.comment.create({
|
const comment = await this.prisma.comment.create({
|
||||||
data: {
|
data: {
|
||||||
content: sanitizedContent,
|
content: sanitizedContent,
|
||||||
|
rawContent: data.content,
|
||||||
userId,
|
userId,
|
||||||
artId,
|
artId,
|
||||||
},
|
},
|
||||||
@@ -193,6 +198,7 @@ export class CommentService {
|
|||||||
const comment = await this.prisma.comment.create({
|
const comment = await this.prisma.comment.create({
|
||||||
data: {
|
data: {
|
||||||
content: sanitizedContent,
|
content: sanitizedContent,
|
||||||
|
rawContent: data.content,
|
||||||
userId,
|
userId,
|
||||||
showId,
|
showId,
|
||||||
},
|
},
|
||||||
@@ -219,6 +225,7 @@ export class CommentService {
|
|||||||
const comment = await this.prisma.comment.create({
|
const comment = await this.prisma.comment.create({
|
||||||
data: {
|
data: {
|
||||||
content: sanitizedContent,
|
content: sanitizedContent,
|
||||||
|
rawContent: data.content,
|
||||||
userId,
|
userId,
|
||||||
mangaId,
|
mangaId,
|
||||||
},
|
},
|
||||||
@@ -227,9 +234,57 @@ export class CommentService {
|
|||||||
return this.mapComment(comment);
|
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> {
|
async deleteComment(commentId: string): Promise<void> {
|
||||||
await this.prisma.comment.delete({
|
await this.prisma.comment.delete({
|
||||||
where: { id: commentId },
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,12 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ auditService.getActionLabel(log.action) }}</td>
|
<td>{{ auditService.getActionLabel(log.action) }}</td>
|
||||||
<td class="details">{{ log.details ?? '-' }}</td>
|
<td class="details" [class.expanded]="expandedRows()[log.id]" (click)="toggleRowExpand(log.id)">
|
||||||
|
<span class="details-content">{{ log.details ?? '-' }}</span>
|
||||||
|
@if (log.details && log.details.length > 50) {
|
||||||
|
<span class="expand-hint">{{ expandedRows()[log.id] ? '(click to collapse)' : '(click to expand)' }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="status-badge" [class.success]="log.success" [class.failed]="!log.success">
|
<span class="status-badge" [class.success]="log.success" [class.failed]="!log.success">
|
||||||
{{ log.success ? '✓' : '✗' }}
|
{{ log.success ? '✓' : '✗' }}
|
||||||
@@ -258,11 +263,33 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types
|
|||||||
|
|
||||||
.details {
|
.details {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details .details-content {
|
||||||
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details.expanded .details-content {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details .expand-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -320,6 +347,7 @@ export class AdminAuditComponent implements OnInit {
|
|||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
currentPage = signal(1);
|
currentPage = signal(1);
|
||||||
totalPages = signal(1);
|
totalPages = signal(1);
|
||||||
|
expandedRows = signal<Record<string, boolean>>({});
|
||||||
|
|
||||||
selectedCategory = '';
|
selectedCategory = '';
|
||||||
selectedAction = '';
|
selectedAction = '';
|
||||||
@@ -370,4 +398,11 @@ export class AdminAuditComponent implements OnInit {
|
|||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
return d.toLocaleString();
|
return d.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleRowExpand(logId: string) {
|
||||||
|
this.expandedRows.set({
|
||||||
|
...this.expandedRows(),
|
||||||
|
[logId]: !this.expandedRows()[logId]
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,11 +239,28 @@ import { Art, CreateArtDto, UpdateArtDto, Comment } from '@library/shared-types'
|
|||||||
}
|
}
|
||||||
<span class="comment-author">{{ comment.user.username }}</span>
|
<span class="comment-author">{{ comment.user.username }}</span>
|
||||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||||
@if (authService.isAdmin()) {
|
@if (canEditComment(comment)) {
|
||||||
|
<button (click)="startEditComment(art.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="deleteComment(art.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="deleteComment(art.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
@if (editingCommentId() === comment.id) {
|
||||||
|
<div class="comment-edit-form">
|
||||||
|
<textarea
|
||||||
|
[(ngModel)]="editCommentContent"
|
||||||
|
name="editComment"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="comment-edit-actions">
|
||||||
|
<button (click)="saveCommentEdit(art.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||||
|
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||||
@@ -639,6 +656,32 @@ import { Art, CreateArtDto, UpdateArtDto, Comment } from '@library/shared-types'
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-edit-form {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 2px solid var(--witch-lavender);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background-color: var(--witch-moon);
|
||||||
|
color: var(--witch-purple);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--witch-rose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class ArtGalleryComponent implements OnInit {
|
export class ArtGalleryComponent implements OnInit {
|
||||||
@@ -658,6 +701,8 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
editingCommentId = signal<string | null>(null);
|
||||||
|
editCommentContent = '';
|
||||||
|
|
||||||
newArt: Partial<CreateArtDto> = {
|
newArt: Partial<CreateArtDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -840,4 +885,42 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditComment(artId: string, comment: Comment) {
|
||||||
|
this.editingCommentId.set(comment.id);
|
||||||
|
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCommentEdit() {
|
||||||
|
this.editingCommentId.set(null);
|
||||||
|
this.editCommentContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCommentEdit(artId: string, commentId: string) {
|
||||||
|
if (!this.editCommentContent.trim()) return;
|
||||||
|
|
||||||
|
this.commentsService.updateCommentOnArt(artId, commentId, this.editCommentContent).subscribe({
|
||||||
|
next: (updatedComment) => {
|
||||||
|
this.comments.set({
|
||||||
|
...this.comments(),
|
||||||
|
[artId]: (this.comments()[artId] || []).map(c =>
|
||||||
|
c.id === commentId ? updatedComment : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
this.cancelCommentEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,11 +346,28 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
|||||||
}
|
}
|
||||||
<span class="comment-author">{{ comment.user.username }}</span>
|
<span class="comment-author">{{ comment.user.username }}</span>
|
||||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||||
@if (authService.isAdmin()) {
|
@if (canEditComment(comment)) {
|
||||||
|
<button (click)="startEditComment(book.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="deleteComment(book.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="deleteComment(book.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
@if (editingCommentId() === comment.id) {
|
||||||
|
<div class="comment-edit-form">
|
||||||
|
<textarea
|
||||||
|
[(ngModel)]="editCommentContent"
|
||||||
|
name="editComment"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="comment-edit-actions">
|
||||||
|
<button (click)="saveCommentEdit(book.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||||
|
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||||
@@ -721,6 +738,32 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-edit-form {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 2px solid var(--witch-lavender);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background-color: var(--witch-moon);
|
||||||
|
color: var(--witch-purple);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--witch-rose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview {
|
.image-preview {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -772,6 +815,8 @@ export class BooksListComponent implements OnInit {
|
|||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
editingCommentId = signal<string | null>(null);
|
||||||
|
editCommentContent = '';
|
||||||
|
|
||||||
// Image upload state
|
// Image upload state
|
||||||
newBookImagePreview = signal<string | null>(null);
|
newBookImagePreview = signal<string | null>(null);
|
||||||
@@ -1038,4 +1083,42 @@ export class BooksListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditComment(bookId: string, comment: Comment) {
|
||||||
|
this.editingCommentId.set(comment.id);
|
||||||
|
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCommentEdit() {
|
||||||
|
this.editingCommentId.set(null);
|
||||||
|
this.editCommentContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCommentEdit(bookId: string, commentId: string) {
|
||||||
|
if (!this.editCommentContent.trim()) return;
|
||||||
|
|
||||||
|
this.commentsService.updateCommentOnBook(bookId, commentId, this.editCommentContent).subscribe({
|
||||||
|
next: (updatedComment) => {
|
||||||
|
this.comments.set({
|
||||||
|
...this.comments(),
|
||||||
|
[bookId]: (this.comments()[bookId] || []).map(c =>
|
||||||
|
c.id === commentId ? updatedComment : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
this.cancelCommentEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -311,11 +311,28 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
|||||||
}
|
}
|
||||||
<span class="comment-author">{{ comment.user.username }}</span>
|
<span class="comment-author">{{ comment.user.username }}</span>
|
||||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||||
@if (authService.isAdmin()) {
|
@if (canEditComment(comment)) {
|
||||||
|
<button (click)="startEditComment(game.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="deleteComment(game.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="deleteComment(game.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
@if (editingCommentId() === comment.id) {
|
||||||
|
<div class="comment-edit-form">
|
||||||
|
<textarea
|
||||||
|
[(ngModel)]="editCommentContent"
|
||||||
|
name="editComment"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="comment-edit-actions">
|
||||||
|
<button (click)="saveCommentEdit(game.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||||
|
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||||
@@ -600,6 +617,25 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-edit-form {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview {
|
.image-preview {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -651,6 +687,8 @@ export class GamesListComponent implements OnInit {
|
|||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
editingCommentId = signal<string | null>(null);
|
||||||
|
editCommentContent = '';
|
||||||
|
|
||||||
// Image upload state
|
// Image upload state
|
||||||
newGameImagePreview = signal<string | null>(null);
|
newGameImagePreview = signal<string | null>(null);
|
||||||
@@ -913,4 +951,42 @@ export class GamesListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditComment(gameId: string, comment: Comment) {
|
||||||
|
this.editingCommentId.set(comment.id);
|
||||||
|
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCommentEdit() {
|
||||||
|
this.editingCommentId.set(null);
|
||||||
|
this.editCommentContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCommentEdit(gameId: string, commentId: string) {
|
||||||
|
if (!this.editCommentContent.trim()) return;
|
||||||
|
|
||||||
|
this.commentsService.updateCommentOnGame(gameId, commentId, this.editCommentContent).subscribe({
|
||||||
|
next: (updatedComment) => {
|
||||||
|
this.comments.set({
|
||||||
|
...this.comments(),
|
||||||
|
[gameId]: (this.comments()[gameId] || []).map(c =>
|
||||||
|
c.id === commentId ? updatedComment : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
this.cancelCommentEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -311,11 +311,28 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment } from '@li
|
|||||||
}
|
}
|
||||||
<span class="comment-author">{{ comment.user.username }}</span>
|
<span class="comment-author">{{ comment.user.username }}</span>
|
||||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||||
@if (authService.isAdmin()) {
|
@if (canEditComment(comment)) {
|
||||||
|
<button (click)="startEditComment(manga.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="deleteComment(manga.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="deleteComment(manga.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
@if (editingCommentId() === comment.id) {
|
||||||
|
<div class="comment-edit-form">
|
||||||
|
<textarea
|
||||||
|
[(ngModel)]="editCommentContent"
|
||||||
|
name="editComment"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="comment-edit-actions">
|
||||||
|
<button (click)="saveCommentEdit(manga.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||||
|
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||||
@@ -602,6 +619,25 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment } from '@li
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-edit-form {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview {
|
.image-preview {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -652,6 +688,8 @@ export class MangaListComponent implements OnInit {
|
|||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
editingCommentId = signal<string | null>(null);
|
||||||
|
editCommentContent = '';
|
||||||
|
|
||||||
newMangaImagePreview = signal<string | null>(null);
|
newMangaImagePreview = signal<string | null>(null);
|
||||||
editMangaImagePreview = signal<string | null>(null);
|
editMangaImagePreview = signal<string | null>(null);
|
||||||
@@ -909,4 +947,42 @@ export class MangaListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditComment(mangaId: string, comment: Comment) {
|
||||||
|
this.editingCommentId.set(comment.id);
|
||||||
|
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCommentEdit() {
|
||||||
|
this.editingCommentId.set(null);
|
||||||
|
this.editCommentContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCommentEdit(mangaId: string, commentId: string) {
|
||||||
|
if (!this.editCommentContent.trim()) return;
|
||||||
|
|
||||||
|
this.commentsService.updateCommentOnManga(mangaId, commentId, this.editCommentContent).subscribe({
|
||||||
|
next: (updatedComment) => {
|
||||||
|
this.comments.set({
|
||||||
|
...this.comments(),
|
||||||
|
[mangaId]: (this.comments()[mangaId] || []).map(c =>
|
||||||
|
c.id === commentId ? updatedComment : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
this.cancelCommentEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,11 +384,28 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
|||||||
}
|
}
|
||||||
<span class="comment-author">{{ comment.user.username }}</span>
|
<span class="comment-author">{{ comment.user.username }}</span>
|
||||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||||
@if (authService.isAdmin()) {
|
@if (canEditComment(comment)) {
|
||||||
|
<button (click)="startEditComment(music.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="deleteComment(music.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="deleteComment(music.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
@if (editingCommentId() === comment.id) {
|
||||||
|
<div class="comment-edit-form">
|
||||||
|
<textarea
|
||||||
|
[(ngModel)]="editCommentContent"
|
||||||
|
name="editComment"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="comment-edit-actions">
|
||||||
|
<button (click)="saveCommentEdit(music.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||||
|
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||||
@@ -793,6 +810,32 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-edit-form {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 2px solid var(--witch-lavender);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background-color: var(--witch-moon);
|
||||||
|
color: var(--witch-purple);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--witch-rose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview {
|
.image-preview {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -845,6 +888,8 @@ export class MusicListComponent implements OnInit {
|
|||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
editingCommentId = signal<string | null>(null);
|
||||||
|
editCommentContent = '';
|
||||||
|
|
||||||
// Image upload state
|
// Image upload state
|
||||||
newMusicImagePreview = signal<string | null>(null);
|
newMusicImagePreview = signal<string | null>(null);
|
||||||
@@ -1137,4 +1182,42 @@ export class MusicListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditComment(musicId: string, comment: Comment) {
|
||||||
|
this.editingCommentId.set(comment.id);
|
||||||
|
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCommentEdit() {
|
||||||
|
this.editingCommentId.set(null);
|
||||||
|
this.editCommentContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCommentEdit(musicId: string, commentId: string) {
|
||||||
|
if (!this.editCommentContent.trim()) return;
|
||||||
|
|
||||||
|
this.commentsService.updateCommentOnMusic(musicId, commentId, this.editCommentContent).subscribe({
|
||||||
|
next: (updatedComment) => {
|
||||||
|
this.comments.set({
|
||||||
|
...this.comments(),
|
||||||
|
[musicId]: (this.comments()[musicId] || []).map(c =>
|
||||||
|
c.id === commentId ? updatedComment : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
this.cancelCommentEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -307,11 +307,28 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment } fro
|
|||||||
}
|
}
|
||||||
<span class="comment-author">{{ comment.user.username }}</span>
|
<span class="comment-author">{{ comment.user.username }}</span>
|
||||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||||
@if (authService.isAdmin()) {
|
@if (canEditComment(comment)) {
|
||||||
|
<button (click)="startEditComment(show.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
|
}
|
||||||
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="deleteComment(show.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="deleteComment(show.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
@if (editingCommentId() === comment.id) {
|
||||||
|
<div class="comment-edit-form">
|
||||||
|
<textarea
|
||||||
|
[(ngModel)]="editCommentContent"
|
||||||
|
name="editComment"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="comment-edit-actions">
|
||||||
|
<button (click)="saveCommentEdit(show.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||||
|
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||||
@@ -597,6 +614,25 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment } fro
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-edit-form {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview {
|
.image-preview {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -647,6 +683,8 @@ export class ShowsListComponent implements OnInit {
|
|||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
editingCommentId = signal<string | null>(null);
|
||||||
|
editCommentContent = '';
|
||||||
|
|
||||||
newShowImagePreview = signal<string | null>(null);
|
newShowImagePreview = signal<string | null>(null);
|
||||||
editShowImagePreview = signal<string | null>(null);
|
editShowImagePreview = signal<string | null>(null);
|
||||||
@@ -914,4 +952,42 @@ export class ShowsListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: Comment): boolean {
|
||||||
|
const user = this.authService.user();
|
||||||
|
if (!user) return false;
|
||||||
|
return comment.userId === user.id || this.authService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditComment(showId: string, comment: Comment) {
|
||||||
|
this.editingCommentId.set(comment.id);
|
||||||
|
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCommentEdit() {
|
||||||
|
this.editingCommentId.set(null);
|
||||||
|
this.editCommentContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCommentEdit(showId: string, commentId: string) {
|
||||||
|
if (!this.editCommentContent.trim()) return;
|
||||||
|
|
||||||
|
this.commentsService.updateCommentOnShow(showId, commentId, this.editCommentContent).subscribe({
|
||||||
|
next: (updatedComment) => {
|
||||||
|
this.comments.set({
|
||||||
|
...this.comments(),
|
||||||
|
[showId]: (this.comments()[showId] || []).map(c =>
|
||||||
|
c.id === commentId ? updatedComment : c
|
||||||
|
)
|
||||||
|
});
|
||||||
|
this.cancelCommentEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,4 +86,28 @@ export class CommentsService {
|
|||||||
deleteCommentFromManga(mangaId: string, commentId: string): Observable<{ success: boolean }> {
|
deleteCommentFromManga(mangaId: string, commentId: string): Observable<{ success: boolean }> {
|
||||||
return this.api.delete<{ success: boolean }>(`/manga/${mangaId}/comments/${commentId}`);
|
return this.api.delete<{ success: boolean }>(`/manga/${mangaId}/comments/${commentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCommentOnGame(gameId: string, commentId: string, content: string): Observable<Comment> {
|
||||||
|
return this.api.put<Comment>(`/games/${gameId}/comments/${commentId}`, { content });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommentOnBook(bookId: string, commentId: string, content: string): Observable<Comment> {
|
||||||
|
return this.api.put<Comment>(`/books/${bookId}/comments/${commentId}`, { content });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommentOnMusic(musicId: string, commentId: string, content: string): Observable<Comment> {
|
||||||
|
return this.api.put<Comment>(`/music/${musicId}/comments/${commentId}`, { content });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommentOnArt(artId: string, commentId: string, content: string): Observable<Comment> {
|
||||||
|
return this.api.put<Comment>(`/art/${artId}/comments/${commentId}`, { content });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommentOnShow(showId: string, commentId: string, content: string): Observable<Comment> {
|
||||||
|
return this.api.put<Comment>(`/shows/${showId}/comments/${commentId}`, { content });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommentOnManga(mangaId: string, commentId: string, content: string): Observable<Comment> {
|
||||||
|
return this.api.put<Comment>(`/manga/${mangaId}/comments/${commentId}`, { content });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export enum AuditAction {
|
|||||||
LOGOUT = "LOGOUT",
|
LOGOUT = "LOGOUT",
|
||||||
LOGIN_FAILED = "LOGIN_FAILED",
|
LOGIN_FAILED = "LOGIN_FAILED",
|
||||||
COMMENT_CREATE = "COMMENT_CREATE",
|
COMMENT_CREATE = "COMMENT_CREATE",
|
||||||
|
COMMENT_UPDATE = "COMMENT_UPDATE",
|
||||||
COMMENT_DELETE = "COMMENT_DELETE",
|
COMMENT_DELETE = "COMMENT_DELETE",
|
||||||
ENTRY_CREATE = "ENTRY_CREATE",
|
ENTRY_CREATE = "ENTRY_CREATE",
|
||||||
ENTRY_UPDATE = "ENTRY_UPDATE",
|
ENTRY_UPDATE = "ENTRY_UPDATE",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface CommentUser {
|
|||||||
export interface Comment {
|
export interface Comment {
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
rawContent?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
user: CommentUser;
|
user: CommentUser;
|
||||||
gameId?: string;
|
gameId?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user