Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -29,8 +28,10 @@ public ResponseEntity<ModerationStatsDTO> getStats() {
}

@GetMapping("/users")
public ResponseEntity<Page<UserModerationDTO>> getAllUsers(Pageable pageable) {
return ResponseEntity.ok(moderationService.getAllUsers(pageable));
public ResponseEntity<Page<UserModerationDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(moderationService.getAllUsers(page, size));
}

@GetMapping("/users/blocked")
Expand Down Expand Up @@ -70,8 +71,11 @@ public ResponseEntity<Void> deleteComment(@PathVariable UUID id, @Authentication
}

@GetMapping("/reports")
public ResponseEntity<List<ReportModerationDTO>> getReports(@RequestParam(required = false) ReportStatus status) {
return ResponseEntity.ok(moderationService.getReports(status));
public ResponseEntity<Page<ReportModerationDTO>> getReports(
@RequestParam(required = false) ReportStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(moderationService.getReports(status, page, size));
}

@GetMapping("/reports/{id}")
Expand All @@ -86,7 +90,9 @@ public ResponseEntity<Void> updateReportStatus(@PathVariable UUID id, @Authentic
}

@GetMapping("/logs")
public ResponseEntity<Page<ModerationLogDTO>> getLogs(Pageable pageable) {
return ResponseEntity.ok(moderationService.getModerationLogs(pageable));
public ResponseEntity<Page<ModerationLogDTO>> getLogs(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(moderationService.getModerationLogs(page, size));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import com.codu.dev.moderation.api.dto.*;
import com.codu.dev.report.enums.ReportStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.UUID;

public interface ModerationService {
ModerationStatsDTO getStats();

Page<UserModerationDTO> getAllUsers(Pageable pageable);
Page<UserModerationDTO> getAllUsers(int page, int size);
List<UserModerationDTO> getBlockedUsers();
void blockUser(UUID userId, UUID moderatorId, BlockUserRequestDTO request);
void unblockUser(UUID userId, UUID moderatorId);
Expand All @@ -20,9 +19,9 @@ public interface ModerationService {
void deletePost(UUID postId, UUID moderatorId, String reason);
void deleteComment(UUID commentId, UUID moderatorId, String reason);

List<ReportModerationDTO> getReports(ReportStatus status);
Page<ReportModerationDTO> getReports(ReportStatus status, int page, int size);
ReportModerationDTO getReport(UUID reportId);
void updateReportStatus(UUID reportId, UUID moderatorId, UpdateReportStatusRequestDTO request);

Page<ModerationLogDTO> getModerationLogs(Pageable pageable);
Page<ModerationLogDTO> getModerationLogs(int page, int size);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import com.codu.dev.report.infra.persistence.repository.ReportRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -42,8 +42,8 @@ public ModerationStatsDTO getStats() {
}

@Override
public Page<UserModerationDTO> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable).map(moderationMapper::toUserDTO);
public Page<UserModerationDTO> getAllUsers(int page, int size) {
return userRepository.findAll(PageRequest.of(page, size)).map(moderationMapper::toUserDTO);
}

@Override
Expand Down Expand Up @@ -112,12 +112,13 @@ public void deleteComment(UUID commentId, UUID moderatorId, String reason) {
}

@Override
public List<ReportModerationDTO> getReports(ReportStatus status) {
List<Report> reports = (status == null)
? reportRepository.findAllByIsDeletedFalse()
: reportRepository.findAllByStatusAndIsDeletedFalseOrderByCreatedAtDesc(status);

return reports.stream().map(moderationMapper::toReportDTO).toList();
public Page<ReportModerationDTO> getReports(ReportStatus status, int page, int size) {
PageRequest pageable = PageRequest.of(page, size);
Page<Report> reports = (status == null)
? reportRepository.findAllByIsDeletedFalseOrderByCreatedAtDesc(pageable)
: reportRepository.findAllByStatusAndIsDeletedFalseOrderByCreatedAtDesc(status, pageable);

return reports.map(moderationMapper::toReportDTO);
}

@Override
Expand Down Expand Up @@ -147,8 +148,8 @@ public void updateReportStatus(UUID reportId, UUID moderatorId, UpdateReportStat
}

@Override
public Page<ModerationLogDTO> getModerationLogs(Pageable pageable) {
return moderationLogRepository.findAllByOrderByCreatedAtDesc(pageable).map(moderationMapper::toLogDTO);
public Page<ModerationLogDTO> getModerationLogs(int page, int size) {
return moderationLogRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(page, size)).map(moderationMapper::toLogDTO);
}

private User findUserById(UUID id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.codu.dev.report.enums.ReportStatus;
import com.codu.dev.report.infra.persistence.entity.Report;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand All @@ -22,6 +24,12 @@ public interface ReportRepository extends JpaRepository<Report, UUID> {
@EntityGraph(attributePaths = {"user", "reporter", "post", "comment", "reviewedBy"})
List<Report> findAllByStatusAndIsDeletedFalseOrderByCreatedAtDesc(ReportStatus status);

@EntityGraph(attributePaths = {"user", "reporter", "post", "comment", "reviewedBy"})
Page<Report> findAllByIsDeletedFalseOrderByCreatedAtDesc(Pageable pageable);

@EntityGraph(attributePaths = {"user", "reporter", "post", "comment", "reviewedBy"})
Page<Report> findAllByStatusAndIsDeletedFalseOrderByCreatedAtDesc(ReportStatus status, Pageable pageable);

long countByStatusAndIsDeletedFalse(ReportStatus status);

long countByIsDeletedFalse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class ModerationServiceSpec extends Specification {
moderationMapper.toUserDTO(user) >> userDto

when: "getting all users"
def result = moderationService.getAllUsers(pageable)
def result = moderationService.getAllUsers(0, 10)

then: "paginated users are returned"
result.content.size() == 1
Expand Down Expand Up @@ -256,32 +256,36 @@ class ModerationServiceSpec extends Specification {

def "should get all reports without status filter"() {
given: "reports exist"
def pageable = PageRequest.of(0, 10)
def report = Report.builder().id(UUID.randomUUID()).reasonContent("Spam").build()
def reportPage = new PageImpl<>([report], pageable, 1)
def reportDto = Mock(ReportModerationDTO)

reportRepository.findAllByIsDeletedFalse() >> [report]
reportRepository.findAllByIsDeletedFalseOrderByCreatedAtDesc(pageable) >> reportPage
moderationMapper.toReportDTO(report) >> reportDto

when: "getting all reports"
def result = moderationService.getReports(null)
def result = moderationService.getReports(null, 0, 10)

then: "all reports are returned"
result.size() == 1
result.content.size() == 1
}

def "should get reports filtered by status"() {
given: "pending reports exist"
def pageable = PageRequest.of(0, 10)
def report = Report.builder().id(UUID.randomUUID()).status(ReportStatus.PENDING).build()
def reportPage = new PageImpl<>([report], pageable, 1)
def reportDto = Mock(ReportModerationDTO)

reportRepository.findAllByStatusAndIsDeletedFalseOrderByCreatedAtDesc(ReportStatus.PENDING) >> [report]
reportRepository.findAllByStatusAndIsDeletedFalseOrderByCreatedAtDesc(ReportStatus.PENDING, pageable) >> reportPage
moderationMapper.toReportDTO(report) >> reportDto

when: "getting pending reports"
def result = moderationService.getReports(ReportStatus.PENDING)
def result = moderationService.getReports(ReportStatus.PENDING, 0, 10)

then: "pending reports are returned"
result.size() == 1
result.content.size() == 1
}

def "should get single report by id"() {
Expand Down Expand Up @@ -383,7 +387,7 @@ class ModerationServiceSpec extends Specification {
moderationMapper.toLogDTO(log) >> logDto

when: "getting logs"
def result = moderationService.getModerationLogs(pageable)
def result = moderationService.getModerationLogs(0, 50)

then: "paginated logs are returned"
result.content.size() == 1
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/(staff)/moderator/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ModerationLogsTable } from "@/features/moderation/moderation-logs-table
import { getLogs } from "@/lib/api/moderation-controller/moderation-controller";

export default async function AuditLogsPage() {
const response = await getLogs({ pageable: { page: 0, size: 10 } });
const response = await getLogs({ page: 0, size: 10 });
const pageData = response.data;

return (
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/app/(staff)/moderator/reports/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ReportsTable } from "@/features/moderation/reports-table";
import { getReports } from "@/lib/api/moderation-controller/moderation-controller";

const PAGE_SIZE = 10;

export default async function ModeratorReportsPage() {
const response = await getReports();
const reports = response.data;
const response = await getReports({ page: 0, size: PAGE_SIZE });
const reports = response.data?.content ?? [];
const totalPages = response.data?.totalPages ?? 1;

return (
<div className="max-w-7xl mx-auto space-y-8">
Expand All @@ -12,7 +15,7 @@ export default async function ModeratorReportsPage() {
<p className="text-text-secondary">Review and resolve user reports.</p>
</div>

<ReportsTable initialReports={reports} />
<ReportsTable initialReports={reports} initialTotalPages={totalPages} />
</div>
);
}
2 changes: 1 addition & 1 deletion frontend/src/app/(staff)/moderator/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UsersTable } from "@/features/moderation/users-table";
import { getAllUsers1 as getAllUsers } from "@/lib/api/moderation-controller/moderation-controller";

export default async function ModeratorUsersPage() {
const response = await getAllUsers({ pageable: { page: 0, size: 10 } });
const response = await getAllUsers({ page: 0, size: 10 });
const pageData = response.data;

return (
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/features/moderation/moderation-logs-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function ModerationLogsTable({
const [currentPage, setCurrentPage] = useState(0);

const { data: logsResponse, isFetching } = useGetLogs({
pageable: { page: currentPage, size: PAGE_SIZE }
page: currentPage,
size: PAGE_SIZE
});

const logs = logsResponse?.data?.content ?? initialLogs;
Expand Down
68 changes: 49 additions & 19 deletions frontend/src/features/moderation/reports-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useNotificationContext } from "@/features/notifications/notification-pr
import { Button } from "@/features/ui/button";
import { Input } from "@/features/ui/input";
import { Modal } from "@/features/ui/modal";
import { PaginationBar } from "@/features/ui/pagination-bar";
import { SegmentedControl } from "@/features/ui/segmented-control";
import {
Table,
Expand All @@ -18,35 +19,66 @@ import {
TableRow
} from "@/features/ui/table";
import { Tag } from "@/features/ui/tag";
import { updateReportStatus } from "@/lib/api/moderation-controller/moderation-controller";
import {
updateReportStatus,
useGetReports
} from "@/lib/api/moderation-controller/moderation-controller";
import {
ReportModerationDTO,
ReportModerationDTOStatus as ReportStatus
} from "@/lib/api/schemas";

const PAGE_SIZE = 10;

interface ReportsTableProps {
initialReports: ReportModerationDTO[];
initialTotalPages: number;
}

interface ModalState {
report: ReportModerationDTO | null;
newStatus: ReportStatus | null;
}

export function ReportsTable({ initialReports }: ReportsTableProps) {
export function ReportsTable({
initialReports,
initialTotalPages
}: ReportsTableProps) {
const { pushToast } = useNotificationContext();
const [reports, setReports] = useState(initialReports);
const [filter, setFilter] = useState<ReportStatus | "ALL">("ALL");
const [currentPage, setCurrentPage] = useState(0);
const [modalState, setModalState] = useState<ModalState>({
report: null,
newStatus: null
});
const [resolutionNotes, setResolutionNotes] = useState("");
const [isLoading, setIsLoading] = useState(false);

const filteredReports = reports.filter(
(r) => filter === "ALL" || r.status === filter
);
const {
data: reportsResponse,
isFetching,
refetch
} = useGetReports({
status: filter === "ALL" ? undefined : filter,
page: currentPage,
size: PAGE_SIZE
});

const reports = reportsResponse?.data?.content ?? initialReports;
const totalPages = reportsResponse?.data?.totalPages ?? initialTotalPages;

const handlePrevious = () => {
setCurrentPage((prev) => Math.max(prev - 1, 0));
};

const handleNext = () => {
setCurrentPage((prev) => prev + 1);
};

const handleFilterChange = (newFilter: ReportStatus | "ALL") => {
setFilter(newFilter);
setCurrentPage(0);
};

const openModal = (report: ReportModerationDTO, status: ReportStatus) => {
setResolutionNotes("");
Expand All @@ -67,17 +99,7 @@ export function ReportsTable({ initialReports }: ReportsTableProps) {
status: modalState.newStatus,
resolutionNotes: resolutionNotes || undefined
});
setReports((prev) =>
prev.map((r) =>
r.id === modalState.report?.id
? {
...r,
status: modalState.newStatus!,
resolutionNotes: resolutionNotes || r.resolutionNotes
}
: r
)
);
refetch();
pushToast({
type: "success",
message: `Report marked as ${modalState.newStatus}`
Expand Down Expand Up @@ -119,7 +141,7 @@ export function ReportsTable({ initialReports }: ReportsTableProps) {
<SegmentedControl
items={["ALL", "PENDING", "RESOLVED", "DISMISSED"] as const}
value={filter}
onChange={setFilter}
onChange={handleFilterChange}
className="min-w-max"
/>
</div>
Expand All @@ -135,7 +157,7 @@ export function ReportsTable({ initialReports }: ReportsTableProps) {
</TableRow>
</TableHeader>
<TableBody>
{filteredReports.map((report) => (
{reports.map((report) => (
<TableRow key={report.id}>
<TableCell>
<div className="flex flex-col">
Expand Down Expand Up @@ -200,6 +222,14 @@ export function ReportsTable({ initialReports }: ReportsTableProps) {
</TableBody>
</Table>

<PaginationBar
currentPage={currentPage}
totalPages={totalPages}
isBusy={isFetching}
onPrevious={handlePrevious}
onNext={handleNext}
/>

<Modal
isOpen={modalState.report !== null}
onClose={closeModal}
Expand Down
Loading
Loading