where('is_active', true)->firstOrFail(); return response()->json([ 'name' => $event->name, 'description' => $event->description, 'slug' => $event->slug, 'is_active' => $event->is_active, 'upload_start_at' => $event->upload_start_at?->toIso8601String(), 'upload_end_at' => $event->upload_end_at?->toIso8601String(), 'max_file_size_mb' => $event->max_file_size_mb, 'allowed_extensions' => $event->allowed_extensions, 'require_password' => $event->require_password, 'has_password' => $event->has_password, ]); } public function verifyPassword(VerifyPasswordRequest $request, string $slug): JsonResponse { $event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail(); if (! $event->require_password) { return response()->json(['verified' => true]); } if (! Hash::check($request->password, $event->upload_password)) { RateLimiter::hit('password-verify:'.$request->ip()); return response()->json(['message' => 'Invalid password'], 401); } return response()->json(['verified' => true]); } public function upload(Request $request, string $slug): JsonResponse { Log::info('Upload request received', [ 'slug' => $slug, 'has_file' => $request->hasFile('file'), 'files' => array_keys($request->allFiles()), 'content_length' => $request->header('Content-Length'), ]); $event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail(); if ($event->require_password) { $password = $request->header('X-Upload-Password'); if (! $password || ! Hash::check($password, $event->upload_password)) { return response()->json(['message' => 'Invalid or missing upload password'], 401); } } $this->validateUploadWindow($event); $request->validate([ 'file' => ['required', 'file'], ]); $file = $request->file('file'); Log::info('File details', [ 'original_name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime' => $file->getMimeType(), 'is_valid' => $file->isValid(), 'error' => $file->getError(), 'temp_path' => $file->getPathname(), ]); $originalName = $file->getClientOriginalName(); $extension = strtolower($file->getClientOriginalExtension() ?: $file->guessExtension()); if (! in_array($extension, $event->allowed_extensions ?? [], true)) { return response()->json([ 'message' => 'File type not allowed. Allowed: '.implode(', ', $event->allowed_extensions), ], 422); } $maxBytes = $event->max_file_size_mb * 1024 * 1024; if ($file->getSize() > $maxBytes) { return response()->json([ 'message' => 'File too large. Maximum size: '.$event->max_file_size_mb.' MB', ], 422); } // Ensure temp directory exists (local disk uses app/private/) $tempDir = storage_path('app/private/uploads/temp'); if (! is_dir($tempDir)) { mkdir($tempDir, 0755, true); } $storedName = Str::uuid().'.'.$extension; try { $tempPath = $file->storeAs('uploads/temp', $storedName, ['disk' => 'local']); if ($tempPath === false || $tempPath === null) { Log::error('File storeAs returned false/null', [ 'original_name' => $originalName, 'stored_name' => $storedName, 'temp_dir' => $tempDir, 'temp_dir_exists' => is_dir($tempDir), 'temp_dir_writable' => is_writable($tempDir), ]); return response()->json(['message' => 'Failed to store file'], 500); } } catch (\Throwable $e) { Log::error('File storage exception', [ 'error' => $e->getMessage(), 'original_name' => $originalName, 'stored_name' => $storedName, ]); return response()->json(['message' => 'Failed to store file: '.$e->getMessage()], 500); } // Local disk stores in app/private/, so construct full path accordingly $fullPath = storage_path('app/private/'.$tempPath); Log::info('File stored successfully', [ 'temp_path' => $tempPath, 'full_path' => $fullPath, 'file_exists' => file_exists($fullPath), ]); $upload = $event->uploads()->create([ 'original_filename' => $originalName, 'stored_filename' => $storedName, 'file_size' => $file->getSize(), 'mime_type' => $file->getMimeType(), 'status' => 'pending', 'uploader_name' => $request->input('uploader_name'), 'uploader_email' => $request->input('uploader_email'), ]); ProcessEventUpload::dispatch($upload, $fullPath); return response()->json([ 'upload_id' => $upload->id, 'status' => $upload->status, ], 201); } public function uploadStatus(string $slug, int $uploadId): JsonResponse { $event = Event::where('slug', $slug)->where('is_active', true)->firstOrFail(); $upload = $event->uploads()->findOrFail($uploadId); return response()->json([ 'id' => $upload->id, 'status' => $upload->status, 'error_message' => $upload->error_message, 'google_drive_web_link' => $upload->google_drive_web_link, ]); } protected function validateUploadWindow(Event $event): void { if ($event->upload_start_at && now()->isBefore($event->upload_start_at)) { abort(422, 'Uploads are not yet open.'); } if ($event->upload_end_at && now()->isAfter($event->upload_end_at)) { abort(422, 'Upload window has closed.'); } } }