diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index f6bed6d..581605f 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -376,7 +376,7 @@ model ProfileReport { model CommentReport { id String @id @default(auto()) @map("_id") @db.ObjectId reportedCommentId String @db.ObjectId - reportedComment Comment @relation(fields: [reportedCommentId], references: [id]) + reportedComment Comment @relation(fields: [reportedCommentId], references: [id], onDelete: Cascade) reporterId String @db.ObjectId reporter User @relation("CommentReporter", fields: [reporterId], references: [id]) reason ReportReason diff --git a/api/src/app/routes/comments/index.ts b/api/src/app/routes/comments/index.ts new file mode 100644 index 0000000..a0893a6 --- /dev/null +++ b/api/src/app/routes/comments/index.ts @@ -0,0 +1,72 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { FastifyPluginAsync } from "fastify"; +import { Comment, AuditAction, AuditCategory } from "@library/shared-types"; +import { CommentService } from "../../services/comment.service"; +import { AuditService } from "../../services/audit.service"; +import { adminGuard } from "../../middleware/admin-guard"; + +interface UpdateCommentBody { + content: string; +} + +const commentsRoutes: FastifyPluginAsync = async (app) => { + const commentService = new CommentService(); + + // Admin: Update any comment by ID + app.put<{ Params: { id: string }; Body: UpdateCommentBody; Reply: Comment | { error: string } }>( + "/:id", + { + preValidation: [app.authenticate, adminGuard], + preHandler: [app.csrfProtection], + }, + async (request, reply) => { + const { id } = request.params; + const { content } = request.body; + + const existingComment = await commentService.getCommentById(id); + if (!existingComment) { + return reply.code(404).send({ error: "Comment not found" }); + } + + const comment = await commentService.updateComment(id, content); + await AuditService.logFromRequest(request, { + action: AuditAction.commentUpdate, + category: AuditCategory.admin, + details: `Admin updated comment ${id}`, + }); + return comment; + } + ); + + // Admin: Delete any comment by ID + app.delete<{ Params: { id: string }; Reply: { success: boolean } | { error: string } }>( + "/:id", + { + preValidation: [app.authenticate, adminGuard], + preHandler: [app.csrfProtection], + }, + async (request, reply) => { + const { id } = request.params; + + const existingComment = await commentService.getCommentById(id); + if (!existingComment) { + return reply.code(404).send({ error: "Comment not found" }); + } + + await commentService.deleteComment(id); + await AuditService.logFromRequest(request, { + action: AuditAction.commentDelete, + category: AuditCategory.admin, + details: `Admin deleted comment ${id}`, + }); + return { success: true }; + } + ); +}; + +export default commentsRoutes; diff --git a/api/src/app/routes/users/index.ts b/api/src/app/routes/users/index.ts index ccb5f51..67e9c43 100644 --- a/api/src/app/routes/users/index.ts +++ b/api/src/app/routes/users/index.ts @@ -231,6 +231,30 @@ const usersRoutes: FastifyPluginAsync = async (app) => { return user; } ); + + app.post<{ Params: { id: string }; Reply: User | { error: string } }>( + "/:id/make-private", + { + preValidation: [app.authenticate, adminGuard], + preHandler: [app.csrfProtection], + }, + async (request, reply) => { + const { id } = request.params; + const user = await userService.updateUserSettings(id, { profilePublic: false }); + if (!user) { + return reply.code(404).send({ error: "User not found" }); + } + + await AuditService.logFromRequest(request, { + action: AuditAction.entryUpdate, + category: AuditCategory.admin, + targetUserId: id, + details: `Admin made profile private for user: ${user.username}`, + }); + + return user; + } + ); }; export default usersRoutes; diff --git a/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts b/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts index 193914f..2f1469a 100644 --- a/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts +++ b/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts @@ -10,6 +10,8 @@ import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { ReportService } from '../../services/report.service'; import { CommentReportService } from '../../services/comment-report.service'; +import { CommentsService } from '../../services/comments.service'; +import { UserService } from '../../services/user.service'; import { AuthService } from '../../services/auth.service'; import { ToastService } from '../../services/toast.service'; import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportReason } from '@library/shared-types'; @@ -1236,6 +1238,8 @@ import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportR export class AdminReportsComponent implements OnInit { private reportService = inject(ReportService); private commentReportService = inject(CommentReportService); + private commentsService = inject(CommentsService); + private userService = inject(UserService); private authService = inject(AuthService); private toastService = inject(ToastService); private router = inject(Router); @@ -1487,40 +1491,66 @@ export class AdminReportsComponent implements OnInit { editComment(report: CommentReportWithDetails): void { const commentId = report.reportedComment.id; - const newContent = prompt('Edit comment content:', report.reportedComment.content); + const currentContent = report.reportedComment.rawContent || report.reportedComment.content; + const newContent = prompt('Edit comment content:', currentContent); if (newContent !== null && newContent.trim() !== '') { - // TODO: Implement comment editing API call - this.toastService.success('Comment edit functionality coming soon'); + this.commentsService.adminUpdateComment(commentId, newContent).subscribe({ + next: () => { + this.toastService.success('Comment updated successfully'); + this.loadReports(); + this.closeCommentReviewModal(); + }, + error: (err) => { + this.toastService.error(err.message ?? 'Failed to update comment'); + } + }); } } deleteComment(report: CommentReportWithDetails): void { + const commentId = report.reportedComment.id; const commentAuthor = report.reportedComment.user.username; const confirmed = confirm(`Are you sure you want to delete this comment by ${commentAuthor}?`); if (confirmed) { - // TODO: Implement comment deletion API call - this.toastService.success('Comment delete functionality coming soon'); + this.commentsService.adminDeleteComment(commentId).subscribe({ + next: () => { + this.toastService.success('Comment deleted successfully'); + this.loadReports(); + this.closeCommentReviewModal(); + }, + error: (err) => { + this.toastService.error(err.message ?? 'Failed to delete comment'); + } + }); } } editProfile(report: ProfileReportWithUsers): void { const userId = report.reportedUser.id; - const username = report.reportedUser.username; - // Navigate to profile edit page or open edit modal - this.router.navigate(['/profile', username]); + // Navigate to profile page using ObjectId + this.router.navigate(['/profile', userId]); this.toastService.success('Navigate to profile to edit'); } makeProfilePrivate(report: ProfileReportWithUsers): void { + const userId = report.reportedUser.id; const username = report.reportedUser.username; const confirmed = confirm(`Are you sure you want to make ${username}'s profile private?`); if (confirmed) { - // TODO: Implement make profile private API call - this.toastService.success('Profile privacy functionality coming soon'); + this.userService.makeProfilePrivate(userId).subscribe({ + next: () => { + this.toastService.success(`${username}'s profile has been made private`); + this.loadReports(); + this.closeProfileReviewModal(); + }, + error: (err) => { + this.toastService.error(err.message ?? 'Failed to update profile privacy'); + } + }); } } } diff --git a/apps/frontend/src/app/services/comments.service.ts b/apps/frontend/src/app/services/comments.service.ts index 454bc6f..7dcc08b 100644 --- a/apps/frontend/src/app/services/comments.service.ts +++ b/apps/frontend/src/app/services/comments.service.ts @@ -111,4 +111,13 @@ export class CommentsService { updateCommentOnManga(mangaId: string, commentId: string, content: string): Observable { return this.api.put(`/manga/${mangaId}/comments/${commentId}`, { content }); } + + // Admin methods - work with comment ID directly + adminUpdateComment(commentId: string, content: string): Observable { + return this.api.put(`/comments/${commentId}`, { content }); + } + + adminDeleteComment(commentId: string): Observable<{ success: boolean }> { + return this.api.delete<{ success: boolean }>(`/comments/${commentId}`); + } } diff --git a/apps/frontend/src/app/services/user.service.ts b/apps/frontend/src/app/services/user.service.ts index 2ac4d6d..93c6ff3 100644 --- a/apps/frontend/src/app/services/user.service.ts +++ b/apps/frontend/src/app/services/user.service.ts @@ -81,4 +81,8 @@ export class UserService { getProfile(identifier: string): Observable { return this.api.get(`/users/profile/${identifier}`); } + + makeProfilePrivate(userId: string): Observable { + return this.api.post(`/users/${userId}/make-private`, {}); + } }