authenticate($username, $password); $event = $setting->event; if (! $event) { throw new SparkboothUploadException('invalid_credentials', 'Invalid credentials', 401); } $this->enforceExpiry($setting); $this->enforceRateLimit($event); $this->assertValidFile($media); $importDisk = config('photobooth.import.disk', 'photobooth'); $basePath = ltrim((string) ($setting->path ?: $this->buildPath($event)), '/'); $extension = strtolower($media->getClientOriginalExtension() ?: $media->extension() ?: 'jpg'); $filename = Str::uuid().'.'.$extension; $relativePath = "{$basePath}/{$filename}"; if (! $setting->path) { $setting->forceFill([ 'path' => $basePath, ])->save(); } Storage::disk($importDisk)->makeDirectory($basePath); Storage::disk($importDisk)->putFileAs($basePath, $media, $filename); $summary = $this->ingestService->ingest($event->fresh('photoboothSetting'), 1, Photo::SOURCE_SPARKBOOTH); if (($summary['processed'] ?? 0) < 1) { throw new SparkboothUploadException('ingest_failed', 'Upload failed, please retry.', 500); } $setting->forceFill([ 'last_upload_at' => now(), 'uploads_last_24h' => ($setting->uploads_last_24h ?? 0) + 1, 'uploads_total' => ($setting->uploads_total ?? 0) + 1, ])->save(); return [ 'event' => $event->fresh('photoboothSetting'), 'processed' => $summary['processed'] ?? 0, 'skipped' => $summary['skipped'] ?? 0, ]; } protected function authenticate(?string $username, ?string $password): EventPhotoboothSetting { if (! $username || ! $password) { throw new SparkboothUploadException('missing_credentials', 'Invalid credentials', 401); } $normalizedUsername = strtolower(trim($username)); /** @var EventPhotoboothSetting|null $setting */ $setting = EventPhotoboothSetting::query() ->where('username', $normalizedUsername) ->with('event') ->first(); if (! $setting) { throw new SparkboothUploadException('invalid_credentials', 'Invalid credentials', 401); } if (! $setting->event) { throw new SparkboothUploadException('invalid_credentials', 'Invalid credentials', 401); } if ($setting->mode !== 'sparkbooth' || ! $setting->enabled) { throw new SparkboothUploadException('disabled', 'Upload not active for this event', 403); } if (! hash_equals($setting->password ?? '', $password ?? '')) { throw new SparkboothUploadException('invalid_credentials', 'Invalid credentials', 401); } return $setting; } protected function enforceExpiry(EventPhotoboothSetting $setting): void { if ($setting->expires_at && $setting->expires_at->isPast()) { throw new SparkboothUploadException('expired', 'Upload access has expired', 403); } } protected function enforceRateLimit(Event $event): void { $limit = (int) (config('photobooth.sparkbooth.rate_limit_per_minute') ?? 0); if ($limit <= 0) { return; } $key = sprintf('sparkbooth:event:%d', $event->id); if (RateLimiter::tooManyAttempts($key, $limit)) { throw new SparkboothUploadException('rate_limited', 'Upload limit reached; try again in a moment.', 429); } RateLimiter::hit($key, 60); } protected function assertValidFile(UploadedFile $file): void { $allowed = config('photobooth.sparkbooth.allowed_extensions', ['jpg', 'jpeg', 'png', 'webp']); $extension = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: ''); if (! $extension || ! in_array($extension, $allowed, true)) { throw new SparkboothUploadException('invalid_file', 'Unsupported file type', 400); } $maxSize = (int) config('photobooth.sparkbooth.max_size_kb', 8192) * 1024; $size = $file->getSize() ?? 0; if ($maxSize > 0 && $size > $maxSize) { throw new SparkboothUploadException('file_too_large', 'File too large', 400); } } protected function buildPath(Event $event): string { $tenantKey = $event->tenant?->slug ?? $event->tenant_id; return trim((string) $tenantKey, '/').'/'.$event->getKey(); } }