234 lines
9.2 KiB
PHP
234 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\NovaCard;
|
|
use App\Models\Report;
|
|
use App\Models\ReportHistory;
|
|
use App\Services\NovaCards\NovaCardPublishModerationService;
|
|
use App\Support\Moderation\ReportTargetResolver;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class ModerationReportQueueController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly ReportTargetResolver $targets,
|
|
private readonly NovaCardPublishModerationService $moderation,
|
|
) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$status = (string) $request->query('status', 'open');
|
|
$status = in_array($status, ['open', 'reviewing', 'closed'], true) ? $status : 'open';
|
|
$group = (string) $request->query('group', '');
|
|
|
|
$query = Report::query()
|
|
->with(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username'])
|
|
->where('status', $status)
|
|
->orderByDesc('id');
|
|
|
|
if ($group === 'nova_cards') {
|
|
$query->whereIn('target_type', $this->targets->novaCardTargetTypes());
|
|
}
|
|
|
|
$items = $query->paginate(30);
|
|
|
|
return response()->json([
|
|
'data' => collect($items->items())
|
|
->map(fn (Report $report): array => $this->serializeReport($report))
|
|
->values()
|
|
->all(),
|
|
'meta' => [
|
|
'current_page' => $items->currentPage(),
|
|
'last_page' => $items->lastPage(),
|
|
'per_page' => $items->perPage(),
|
|
'total' => $items->total(),
|
|
'from' => $items->firstItem(),
|
|
'to' => $items->lastItem(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request, Report $report): JsonResponse
|
|
{
|
|
$data = $request->validate([
|
|
'status' => 'sometimes|in:open,reviewing,closed',
|
|
'moderator_note' => 'sometimes|nullable|string|max:2000',
|
|
]);
|
|
|
|
$before = [];
|
|
$after = [];
|
|
$user = $request->user();
|
|
|
|
DB::transaction(function () use ($data, $report, $user, &$before, &$after): void {
|
|
if (array_key_exists('status', $data) && $data['status'] !== $report->status) {
|
|
$before['status'] = (string) $report->status;
|
|
$after['status'] = (string) $data['status'];
|
|
$report->status = $data['status'];
|
|
}
|
|
|
|
if (array_key_exists('moderator_note', $data)) {
|
|
$normalizedNote = is_string($data['moderator_note']) ? trim($data['moderator_note']) : null;
|
|
$normalizedNote = $normalizedNote !== '' ? $normalizedNote : null;
|
|
|
|
if ($normalizedNote !== $report->moderator_note) {
|
|
$before['moderator_note'] = $report->moderator_note;
|
|
$after['moderator_note'] = $normalizedNote;
|
|
$report->moderator_note = $normalizedNote;
|
|
}
|
|
}
|
|
|
|
if ($before !== [] || $after !== []) {
|
|
$report->last_moderated_by_id = $user?->id;
|
|
$report->last_moderated_at = now();
|
|
$report->save();
|
|
|
|
$report->historyEntries()->create([
|
|
'actor_user_id' => $user?->id,
|
|
'action_type' => 'report_updated',
|
|
'summary' => $this->buildUpdateSummary($before, $after),
|
|
'note' => $report->moderator_note,
|
|
'before_json' => $before !== [] ? $before : null,
|
|
'after_json' => $after !== [] ? $after : null,
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
});
|
|
|
|
$report = $report->fresh(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username']);
|
|
|
|
return response()->json([
|
|
'report' => $this->serializeReport($report),
|
|
]);
|
|
}
|
|
|
|
public function moderateTarget(Request $request, Report $report): JsonResponse
|
|
{
|
|
$data = $request->validate([
|
|
'action' => 'required|in:approve_card,flag_card,reject_card',
|
|
'disposition' => 'nullable|in:' . implode(',', array_keys(NovaCardPublishModerationService::DISPOSITION_LABELS)),
|
|
]);
|
|
|
|
$card = $this->targets->resolveModerationCard($report);
|
|
abort_unless($card !== null, 422, 'This report does not have a Nova Card moderation target.');
|
|
|
|
DB::transaction(function () use ($card, $data, $report, $request): void {
|
|
$before = [
|
|
'card_id' => (int) $card->id,
|
|
'moderation_status' => (string) $card->moderation_status,
|
|
];
|
|
|
|
$nextStatus = match ($data['action']) {
|
|
'approve_card' => NovaCard::MOD_APPROVED,
|
|
'flag_card' => NovaCard::MOD_FLAGGED,
|
|
'reject_card' => NovaCard::MOD_REJECTED,
|
|
};
|
|
$card = $this->moderation->recordStaffOverride(
|
|
$card,
|
|
$nextStatus,
|
|
$request->user(),
|
|
'report_queue',
|
|
[
|
|
'note' => $report->moderator_note,
|
|
'report_id' => $report->id,
|
|
'disposition' => $data['disposition'] ?? null,
|
|
],
|
|
);
|
|
|
|
$report->last_moderated_by_id = $request->user()?->id;
|
|
$report->last_moderated_at = now();
|
|
$report->save();
|
|
|
|
$report->historyEntries()->create([
|
|
'actor_user_id' => $request->user()?->id,
|
|
'action_type' => 'target_moderated',
|
|
'summary' => $this->buildTargetModerationSummary($data['action'], $card),
|
|
'note' => $report->moderator_note,
|
|
'before_json' => $before,
|
|
'after_json' => [
|
|
'card_id' => (int) $card->id,
|
|
'moderation_status' => (string) $card->moderation_status,
|
|
'action' => (string) $data['action'],
|
|
],
|
|
'created_at' => now(),
|
|
]);
|
|
});
|
|
|
|
$report = $report->fresh(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username']);
|
|
|
|
return response()->json([
|
|
'report' => $this->serializeReport($report),
|
|
]);
|
|
}
|
|
|
|
private function buildUpdateSummary(array $before, array $after): string
|
|
{
|
|
$parts = [];
|
|
|
|
if (array_key_exists('status', $after)) {
|
|
$parts[] = sprintf('Status %s -> %s', $before['status'], $after['status']);
|
|
}
|
|
|
|
if (array_key_exists('moderator_note', $after)) {
|
|
$parts[] = $after['moderator_note'] ? 'Moderator note updated' : 'Moderator note cleared';
|
|
}
|
|
|
|
return $parts !== [] ? implode(' • ', $parts) : 'Report reviewed';
|
|
}
|
|
|
|
private function buildTargetModerationSummary(string $action, NovaCard $card): string
|
|
{
|
|
return match ($action) {
|
|
'approve_card' => sprintf('Approved card #%d', $card->id),
|
|
'flag_card' => sprintf('Flagged card #%d', $card->id),
|
|
'reject_card' => sprintf('Rejected card #%d', $card->id),
|
|
default => sprintf('Updated card #%d', $card->id),
|
|
};
|
|
}
|
|
|
|
private function serializeReport(Report $report): array
|
|
{
|
|
return [
|
|
'id' => (int) $report->id,
|
|
'status' => (string) $report->status,
|
|
'target_type' => (string) $report->target_type,
|
|
'target_id' => (int) $report->target_id,
|
|
'reason' => (string) $report->reason,
|
|
'details' => $report->details,
|
|
'moderator_note' => $report->moderator_note,
|
|
'created_at' => optional($report->created_at)?->toISOString(),
|
|
'updated_at' => optional($report->updated_at)?->toISOString(),
|
|
'last_moderated_at' => optional($report->last_moderated_at)?->toISOString(),
|
|
'reporter' => $report->reporter ? [
|
|
'id' => (int) $report->reporter->id,
|
|
'username' => (string) $report->reporter->username,
|
|
] : null,
|
|
'last_moderated_by' => $report->lastModeratedBy ? [
|
|
'id' => (int) $report->lastModeratedBy->id,
|
|
'username' => (string) $report->lastModeratedBy->username,
|
|
] : null,
|
|
'target' => $this->targets->summarize($report),
|
|
'history' => $report->historyEntries
|
|
->take(8)
|
|
->map(fn (ReportHistory $entry): array => [
|
|
'id' => (int) $entry->id,
|
|
'action_type' => (string) $entry->action_type,
|
|
'summary' => $entry->summary,
|
|
'note' => $entry->note,
|
|
'before' => $entry->before_json,
|
|
'after' => $entry->after_json,
|
|
'created_at' => optional($entry->created_at)?->toISOString(),
|
|
'actor' => $entry->actor ? [
|
|
'id' => (int) $entry->actor->id,
|
|
'username' => (string) $entry->actor->username,
|
|
] : null,
|
|
])
|
|
->values()
|
|
->all(),
|
|
];
|
|
}
|
|
}
|