with(['user:id,name,username', 'category:id,name,slug']) ->where('status', 'draft') ->where('moderation_status', 'pending') ->orderBy('created_at') ->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); if (! $upload) { return response()->json(['message' => 'Upload not found.'], Response::HTTP_NOT_FOUND); } $upload->moderation_status = 'approved'; $upload->moderated_at = now(); $upload->moderated_by = (int) $request->user()->id; $upload->moderation_note = $request->input('note'); $upload->save(); return response()->json([ 'success' => true, 'id' => (string) $upload->id, 'moderation_status' => (string) $upload->moderation_status, ], Response::HTTP_OK); } public function reject(string $id, Request $request): JsonResponse { $upload = Upload::query()->find($id); if (! $upload) { return response()->json(['message' => 'Upload not found.'], Response::HTTP_NOT_FOUND); } $upload->moderation_status = 'rejected'; $upload->status = 'rejected'; $upload->processing_state = 'rejected'; $upload->moderated_at = now(); $upload->moderated_by = (int) $request->user()->id; $upload->moderation_note = (string) $request->input('note', ''); $upload->save(); return response()->json([ 'success' => true, 'id' => (string) $upload->id, 'status' => (string) $upload->status, 'processing_state' => (string) $upload->processing_state, '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; } }