Skip to content

Commit 968f91a

Browse files
committed
feat: add comment deletion functionality with user authentication
1 parent 7afabbf commit 968f91a

5 files changed

Lines changed: 169 additions & 23 deletions

File tree

components/Comments/container.vue

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<template>
2-
<div class="flex flex-col gap-2 bg-accented h-[20rem]">
2+
<div class="flex flex-col gap-2 bg-accented h-[25rem]">
33

4-
<!-- TEXTBOX -->
5-
<div class="p-3 flex flex-col flex-shrink-0 w-full">
4+
<!-- COMMENT INPUT -->
5+
<div class="p-3 flex-shrink-0 w-full">
66
<div v-if="isLoggedIn == true" class="flex flex-col w-full items-end gap-2">
77
<UTextarea class="flex-1 w-full" v-model="newComment" placeholder="Add a comment..." />
88
<UButton @click="submitComment">Submit</UButton>
@@ -13,23 +13,34 @@
1313
</div>
1414
</div>
1515

16+
<USeparator class="h-1 opacity-50 px-[2rem]" color="primary" />
1617

17-
<div v-if="pending">
18-
<p>Loading comments...</p>
19-
</div>
18+
<!-- COMMENTS CONTAINER -->
19+
<div class="overflow-y-auto flex-1">
20+
<div v-if="pending">
21+
<p>Loading comments...</p>
22+
</div>
2023

21-
<!-- COMMENTS -->
22-
<div v-else v-if="comments" v-for="data in comments" :key="data.id" class="p-3 flex items-start gap-2">
23-
<UAvatar size="2xl" :src="data.user_avatar" />
24-
<div class="flex flex-col">
25-
<p class="text-sm text-current/80">@{{ data.username }}</p>
26-
<p class="text-xs text-current/50 mb-1">{{ new Date(data.date_added).toLocaleDateString() }}</p>
27-
<p>{{ data.comment }}</p>
24+
<div v-else v-if="comments" v-for="data in comments" :key="data.id"
25+
class="p-3 flex items-start justify-between gap-2">
26+
<div class="flex items-start gap-2 flex-1">
27+
<UAvatar size="2xl" :src="data.user_avatar" icon="i-lucide-circle-user-round"
28+
class="outline outline-old-neutral-500 rounded-xl overflow-hidden" />
29+
<div class="flex flex-col">
30+
<p class="text-md text-current font-semibold">{{ data.username }}</p>
31+
<p class="text-xs text-current/50 mb-1">{{ new Date(data.date_added).toLocaleDateString() }}</p>
32+
<p>{{ data.comment }}</p>
33+
</div>
34+
</div>
35+
36+
<!-- Delete Button -->
37+
<UButton v-if="user?.id === data.user_id.toString()" icon="i-lucide-trash" variant="ghost" color="neutral" size="sm"
38+
@click="deleteComment(data.id)" />
2839
</div>
29-
</div>
3040

31-
<div v-if="!comments || comments.length === 0" class="p-3 text-center text-current/50">
32-
No comments yet. Be the first to comment!
41+
<div v-if="!comments || comments.length === 0" class="p-3 text-center text-current/50">
42+
No comments yet. Be the first to comment!
43+
</div>
3344
</div>
3445
</div>
3546
</template>
@@ -39,6 +50,7 @@ import { useAuthStore } from '~/stores/auth';
3950
4051
const authStore = useAuthStore();
4152
const isLoggedIn = computed(() => authStore.isLoggedIn);
53+
const user = computed(() => authStore.user);
4254
const isLoading = ref(false);
4355
const toast = useToast();
4456
const props = defineProps({
@@ -115,7 +127,7 @@ async function submitComment() {
115127
newComment.value = '';
116128
// Fetch updated comments
117129
await refresh();
118-
130+
119131
toast.add({
120132
title: 'Success',
121133
description: 'Comment added successfully.',
@@ -134,6 +146,43 @@ async function submitComment() {
134146
}
135147
}
136148
149+
// Delete Comment function
150+
async function deleteComment(commentId: number) {
151+
if (!isLoggedIn.value) {
152+
toast.add({
153+
title: 'Error',
154+
description: 'You must be logged in to delete comments.',
155+
color: 'error',
156+
duration: 3000
157+
});
158+
return;
159+
}
160+
161+
try {
162+
await $fetch(`/api/manga/${manga_id}/${chapter_id ? chapter_id + '/' : ''}comments/${commentId}`, {
163+
method: 'DELETE'
164+
});
165+
166+
// Refresh comments after deletion
167+
await refresh();
168+
169+
toast.add({
170+
title: 'Success',
171+
description: 'Comment deleted successfully.',
172+
color: 'success',
173+
duration: 3000
174+
});
175+
} catch (error) {
176+
console.error('Failed to delete comment:', error);
177+
toast.add({
178+
title: 'Error',
179+
description: 'Failed to delete comment.',
180+
color: 'error',
181+
duration: 5000
182+
});
183+
}
184+
}
185+
137186
onMounted(() => {
138187
})
139188
</script>

pages/manga/[id]/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117

118118
<!-- COMMENTS -->
119119
<p class="text-xl font-semibold mt-5">Comments</p>
120-
<div class="max-h-[20rem] overflow-y-scroll rounded-md">
120+
<div class="rounded-md">
121121
<CommentsContainer :manga_id="manga_id" />
122122
</div>
123123
</div>

public/assets/css/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
@import "tailwindcss";
22
@import "@nuxt/ui";
33

4+
.bg-accented {
5+
@apply dark:bg-zinc-800
6+
}
7+
48
.container {
59
width: 100%; /* Default 100% lebar */
610
padding-left: 15px; /* Padding default untuk mobile */

server/api/manga/[mangaID]/[chapterID]/comments/index.delete.ts renamed to server/api/manga/[mangaID]/[chapterID]/comments/[commentID].delete.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @description Deletes a specific comment. This endpoint requires the user to be authenticated.
44
* A user can only delete their own comments.
55
*
6+
* @param {string} commentID - The ID of the comment to delete.
67
* @param {string} mangaID - The ID of the manga (used for URL structure, not in logic).
78
* @param {string} chapterID - The ID of the chapter (used for URL structure, not in logic).
89
*
@@ -48,11 +49,10 @@ export default defineEventHandler(async (event) => {
4849
});
4950
}
5051

51-
// 2. Read comment ID from request body
52-
const body = await readBody(event);
53-
const { commentId } = body;
52+
// 2. Read comment ID from params
53+
let commentID = getRouterParam(event, 'commentID')
5454

55-
if (!commentId) {
55+
if (!commentID) {
5656
throw createError({
5757
statusCode: 400,
5858
message: "Comment ID is required in the request body.",
@@ -64,7 +64,7 @@ export default defineEventHandler(async (event) => {
6464
const result = await db.query(
6565
`DELETE FROM chapter_comments
6666
WHERE id = $1 AND user_ID = $2`,
67-
[commentId, userID]
67+
[commentID, userID]
6868
);
6969

7070
// Check if any row was actually deleted
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @summary Delete a user's comment.
3+
* @description Deletes a specific comment. This endpoint requires the user to be authenticated.
4+
* A user can only delete their own comments.
5+
*
6+
* @param {string} commentID - The ID of the comment to delete.
7+
* @param {string} mangaID - The ID of the manga (used for URL structure, not in logic).
8+
*
9+
* @body {{ commentId: number }} - The request body must be a JSON object containing the ID of the comment to be deleted.
10+
*
11+
* @returns {200, { message: string }} - On success, returns a confirmation message.
12+
* @returns {400} - If the commentId is missing from the request body.
13+
* @returns {401} - If the user is not authenticated or the token is invalid.
14+
* @returns {403} - If the user tries to delete a comment that does not exist or does not belong to them.
15+
* @returns {500} - For any other server-side errors.
16+
*/
17+
18+
import jwt from "jsonwebtoken";
19+
import { db } from "~/server/utils/db";
20+
21+
export default defineEventHandler(async (event) => {
22+
// 1. Get User ID from JWT token (Authentication)
23+
const token = getCookie(event, "auth-token");
24+
if (!token) {
25+
throw createError({
26+
statusCode: 401,
27+
message: "Authentication token is required",
28+
});
29+
}
30+
31+
let userID: string;
32+
try {
33+
const JWT_SECRET = process.env.JWT_SECRET;
34+
if (!JWT_SECRET) {
35+
throw new Error("Server configuration error: JWT_SECRET is not set.");
36+
}
37+
const decoded = jwt.verify(token, JWT_SECRET);
38+
if (typeof decoded === "object" && decoded !== null && "id" in decoded) {
39+
userID = (decoded as jwt.JwtPayload).id;
40+
} else {
41+
throw new Error("Invalid token payload.");
42+
}
43+
} catch (error) {
44+
console.error("JWT verification failed:", error);
45+
throw createError({
46+
statusCode: 401,
47+
message: "Invalid or expired authentication token",
48+
});
49+
}
50+
51+
// 2. Read comment ID from params
52+
let commentID = getRouterParam(event, 'commentID')
53+
54+
if (!commentID) {
55+
throw createError({
56+
statusCode: 400,
57+
message: "Comment ID is required in the request body.",
58+
});
59+
}
60+
61+
// 3. Delete from database, ensuring the user owns the comment
62+
try {
63+
const result = await db.query(
64+
`DELETE FROM manga_comments
65+
WHERE id = $1 AND user_ID = $2`,
66+
[commentID, userID]
67+
);
68+
69+
// Check if any row was actually deleted
70+
if (result.rowCount === 0) {
71+
// This happens if the comment doesn't exist OR the user doesn't own it.
72+
// For security, we don't tell the user which one it is.
73+
throw createError({
74+
statusCode: 403, // Forbidden
75+
message: "Comment not found or you do not have permission to delete it.",
76+
});
77+
}
78+
79+
return {
80+
message: "Comment deleted successfully",
81+
};
82+
} catch (error: any) {
83+
// Re-throw specific errors, otherwise throw a generic 500
84+
if (error.statusCode) {
85+
throw error;
86+
}
87+
console.error("Error deleting comment:", error);
88+
throw createError({
89+
statusCode: 500,
90+
message: "An unexpected error occurred while deleting the comment.",
91+
});
92+
}
93+
});

0 commit comments

Comments
 (0)