Send new artwork uploads through a trust-based review policy.
Require review for untrusted accounts, add admin artwork review APIs, and keep queued or auto-trusted publishes from counting as established history.
This commit is contained in:
@@ -8,33 +8,51 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Upload;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
final class UploadModerationController extends Controller
|
||||
{
|
||||
public function pending(): JsonResponse
|
||||
{
|
||||
$uploads = Upload::query()
|
||||
->with(['user:id,name,username', 'category:id,name,slug'])
|
||||
->where('status', 'draft')
|
||||
->where('moderation_status', 'pending')
|
||||
->orderBy('created_at')
|
||||
->get([
|
||||
'id',
|
||||
'user_id',
|
||||
'type',
|
||||
'status',
|
||||
'processing_state',
|
||||
'title',
|
||||
'preview_path',
|
||||
'created_at',
|
||||
'moderation_status',
|
||||
]);
|
||||
->get()
|
||||
->map(fn (Upload $upload): array => $this->present($upload))
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $uploads,
|
||||
], Response::HTTP_OK);
|
||||
}
|
||||
|
||||
public function preview(string $id): StreamedResponse|JsonResponse
|
||||
{
|
||||
$upload = Upload::query()
|
||||
->where('status', 'draft')
|
||||
->where('moderation_status', 'pending')
|
||||
->find($id);
|
||||
|
||||
if (! $upload) {
|
||||
return response()->json(['message' => 'Upload not found.'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $this->safePreviewPath($upload);
|
||||
if ($path === null || ! Storage::disk('local')->exists($path)) {
|
||||
return response()->json(['message' => 'Preview not found.'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return Storage::disk('local')->response($path, 'preview.webp', [
|
||||
'Content-Type' => 'image/webp',
|
||||
'Cache-Control' => 'private, max-age=120',
|
||||
]);
|
||||
}
|
||||
|
||||
public function approve(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$upload = Upload::query()->find($id);
|
||||
@@ -80,4 +98,56 @@ final class UploadModerationController extends Controller
|
||||
'moderation_status' => (string) $upload->moderation_status,
|
||||
], Response::HTTP_OK);
|
||||
}
|
||||
|
||||
private function present(Upload $upload): array
|
||||
{
|
||||
$tags = collect($upload->tags ?? [])
|
||||
->map(function (mixed $tag): string {
|
||||
if (is_array($tag)) {
|
||||
return trim((string) ($tag['name'] ?? $tag['slug'] ?? ''));
|
||||
}
|
||||
|
||||
return trim((string) $tag);
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$hasPreview = $this->safePreviewPath($upload) !== null;
|
||||
|
||||
return [
|
||||
'id' => (string) $upload->id,
|
||||
'title' => (string) ($upload->title ?: '(untitled upload)'),
|
||||
'description' => (string) ($upload->description ?? ''),
|
||||
'type' => (string) ($upload->type ?? 'image'),
|
||||
'preview_url' => $hasPreview
|
||||
? URL::temporarySignedRoute('api.admin.uploads.preview', now()->addMinutes(30), ['id' => $upload->id])
|
||||
: null,
|
||||
'preview_lg_url' => $hasPreview
|
||||
? URL::temporarySignedRoute('api.admin.uploads.preview', now()->addMinutes(30), ['id' => $upload->id])
|
||||
: null,
|
||||
'tags' => $tags,
|
||||
'categories' => $upload->category?->name ? [(string) $upload->category->name] : [],
|
||||
'user' => $upload->user ? [
|
||||
'id' => (int) $upload->user->id,
|
||||
'name' => (string) $upload->user->name,
|
||||
'username' => $upload->user->username,
|
||||
] : null,
|
||||
'nsfw' => (bool) $upload->nsfw,
|
||||
'license' => $upload->license,
|
||||
'created_at' => optional($upload->created_at)?->toISOString(),
|
||||
];
|
||||
}
|
||||
|
||||
private function safePreviewPath(Upload $upload): ?string
|
||||
{
|
||||
$path = str_replace('\\', '/', ltrim((string) $upload->preview_path, '/'));
|
||||
$expectedPrefix = 'tmp/drafts/'.$upload->id.'/';
|
||||
|
||||
if ($path === '' || str_contains($path, '..') || ! str_starts_with($path, $expectedPrefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user