feat: implement admin action APIs for comment and profile moderation

Added full API implementation for admin action buttons in the report
management interface:

Backend Changes:
- Created new /api/comments routes for admin-only comment operations
- Added PUT /api/comments/:id for admin comment editing
- Added DELETE /api/comments/:id for admin comment deletion
- Added POST /api/users/:id/make-private for admin profile privacy control
- All endpoints protected with adminGuard and csrfProtection
- Proper audit logging for all admin actions
- Added onDelete: Cascade to CommentReport relation for safe comment deletion

Frontend Changes:
- Added adminUpdateComment() and adminDeleteComment() to CommentsService
- Added makeProfilePrivate() to UserService
- Integrated API calls into admin-reports component methods
- editComment() now updates comment via API and refreshes report list
- deleteComment() now deletes comment via API and refreshes report list
- makeProfilePrivate() now updates profile privacy via API
- editProfile() navigates using ObjectId instead of Discord username
- All actions show success/error toasts and close modals on completion

The admin interface now has full working moderation capabilities for both
comment and profile reports.
This commit is contained in:
2026-02-19 19:58:13 -08:00
committed by Naomi Carrigan
parent 8837055e97
commit 6e884b9ae8
6 changed files with 150 additions and 11 deletions
@@ -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');
}
});
}
}
}
@@ -111,4 +111,13 @@ export class CommentsService {
updateCommentOnManga(mangaId: string, commentId: string, content: string): Observable<Comment> {
return this.api.put<Comment>(`/manga/${mangaId}/comments/${commentId}`, { content });
}
// Admin methods - work with comment ID directly
adminUpdateComment(commentId: string, content: string): Observable<Comment> {
return this.api.put<Comment>(`/comments/${commentId}`, { content });
}
adminDeleteComment(commentId: string): Observable<{ success: boolean }> {
return this.api.delete<{ success: boolean }>(`/comments/${commentId}`);
}
}
@@ -81,4 +81,8 @@ export class UserService {
getProfile(identifier: string): Observable<UserProfileResponse> {
return this.api.get<UserProfileResponse>(`/users/profile/${identifier}`);
}
makeProfilePrivate(userId: string): Observable<User> {
return this.api.post<User>(`/users/${userId}/make-private`, {});
}
}