loadMissing(['tenant', 'photoboothSetting']); return DB::transaction(function () use ($event, $settings) { $eventSetting = $this->resolveEventSetting($event); $username = $this->generateUniqueUsername($event, $settings); $password = $this->credentialGenerator->generatePassword(); $path = $this->buildPath($event); $expiresAt = $this->resolveExpiry($event, $settings); $payload = [ 'username' => $username, 'password' => $password, 'path' => $path, 'rate_limit_per_minute' => $settings->rate_limit_per_minute, 'expires_at' => $expiresAt?->toIso8601String(), 'ftp_port' => $settings->ftp_port, ]; $this->client->provisionUser($payload, $settings); $eventSetting->forceFill([ 'enabled' => true, 'mode' => 'ftp', 'username' => $username, 'password' => $password, 'path' => $path, 'expires_at' => $expiresAt, 'status' => 'active', 'last_provisioned_at' => now(), 'metadata' => [ 'rate_limit_per_minute' => $settings->rate_limit_per_minute, ], ])->save(); return tap($event->refresh()->load('photoboothSetting'), function (Event $refreshed) use ($password) { if ($refreshed->photoboothSetting) { $refreshed->photoboothSetting->setAttribute('plain_password', $password); } }); }); } public function rotate(Event $event, ?PhotoboothSetting $settings = null): Event { $settings ??= PhotoboothSetting::current(); $event->loadMissing('photoboothSetting'); $eventSetting = $event->photoboothSetting; if (! $eventSetting || ! $eventSetting->enabled || ! $eventSetting->username) { return $this->enable($event, $settings); } return DB::transaction(function () use ($event, $settings, $eventSetting) { $password = $this->credentialGenerator->generatePassword(); $expiresAt = $this->resolveExpiry($event, $settings); $payload = [ 'password' => $password, 'expires_at' => $expiresAt?->toIso8601String(), 'rate_limit_per_minute' => $settings->rate_limit_per_minute, ]; $this->client->rotateUser($eventSetting->username, $payload, $settings); $eventSetting->forceFill([ 'enabled' => true, 'mode' => 'ftp', 'password' => $password, 'expires_at' => $expiresAt, 'status' => 'active', 'path' => $eventSetting->path ?: $this->buildPath($event), 'last_provisioned_at' => now(), ])->save(); return tap($event->refresh()->load('photoboothSetting'), function (Event $refreshed) use ($password) { if ($refreshed->photoboothSetting) { $refreshed->photoboothSetting->setAttribute('plain_password', $password); } }); }); } public function disable(Event $event, ?PhotoboothSetting $settings = null): Event { $event->loadMissing('photoboothSetting'); $eventSetting = $event->photoboothSetting; if (! $eventSetting || ! $eventSetting->username) { return $event; } $settings ??= PhotoboothSetting::current(); return DB::transaction(function () use ($event, $settings, $eventSetting) { try { $this->client->deleteUser($eventSetting->username, $settings); } catch (\Throwable $exception) { Log::warning('Photobooth account deletion failed', [ 'event_id' => $event->id, 'username' => $eventSetting->username, 'message' => $exception->getMessage(), ]); } $eventSetting->forceFill([ 'enabled' => false, 'status' => 'inactive', 'username' => null, 'password' => null, 'path' => null, 'expires_at' => null, 'last_deprovisioned_at' => now(), ])->save(); return $event->refresh()->load('photoboothSetting'); }); } public function enableSparkbooth(Event $event, ?PhotoboothSetting $settings = null): Event { $settings ??= PhotoboothSetting::current(); $event->loadMissing(['tenant', 'photoboothSetting']); return DB::transaction(function () use ($event, $settings) { $eventSetting = $this->resolveEventSetting($event); $username = $this->generateUniqueUsername($event, $settings); $password = $this->credentialGenerator->generatePassword(); $path = $this->buildPath($event); $expiresAt = $this->resolveExpiry($event, $settings); $eventSetting->forceFill([ 'enabled' => true, 'mode' => 'sparkbooth', 'username' => $username, 'password' => $password, 'expires_at' => $expiresAt, 'status' => 'active', 'path' => $path, 'uploads_last_24h' => 0, 'last_provisioned_at' => now(), ])->save(); return tap($event->refresh()->load('photoboothSetting'), function (Event $refreshed) use ($password) { if ($refreshed->photoboothSetting) { $refreshed->photoboothSetting->setAttribute('plain_password', $password); } }); }); } public function rotateSparkbooth(Event $event, ?PhotoboothSetting $settings = null): Event { $settings ??= PhotoboothSetting::current(); $event->loadMissing('photoboothSetting'); $eventSetting = $event->photoboothSetting; if (! $eventSetting || $eventSetting->mode !== 'sparkbooth' || ! $eventSetting->username) { return $this->enableSparkbooth($event, $settings); } return DB::transaction(function () use ($event, $settings, $eventSetting) { $password = $this->credentialGenerator->generatePassword(); $expiresAt = $this->resolveExpiry($event, $settings); $eventSetting->forceFill([ 'enabled' => true, 'mode' => 'sparkbooth', 'password' => $password, 'expires_at' => $expiresAt, 'status' => 'active', 'path' => $eventSetting->path ?: $this->buildPath($event), 'last_provisioned_at' => now(), ])->save(); return tap($event->refresh()->load('photoboothSetting'), function (Event $refreshed) use ($password) { if ($refreshed->photoboothSetting) { $refreshed->photoboothSetting->setAttribute('plain_password', $password); } }); }); } public function disableSparkbooth(Event $event): Event { $event->loadMissing('photoboothSetting'); $eventSetting = $event->photoboothSetting; if (! $eventSetting) { return $event; } return DB::transaction(function () use ($event, $eventSetting) { $eventSetting->forceFill([ 'enabled' => false, 'mode' => 'ftp', 'username' => null, 'password' => null, 'expires_at' => null, 'status' => 'inactive', 'last_upload_at' => null, 'uploads_last_24h' => 0, 'uploads_total' => 0, ])->save(); return $event->refresh()->load('photoboothSetting'); }); } protected function resolveEventSetting(Event $event): EventPhotoboothSetting { $event->loadMissing('photoboothSetting'); return $event->photoboothSetting ?? $event->photoboothSetting()->make(); } protected function resolveExpiry(Event $event, PhotoboothSetting $settings): CarbonInterface { $eventEnd = $event->date ? Carbon::parse($event->date) : now(); $graceDays = max(0, (int) $settings->expiry_grace_days); return $eventEnd->copy() ->endOfDay() ->addDays($graceDays); } protected function generateUniqueUsername(Event $event, PhotoboothSetting $settings): string { $maxAttempts = 10; for ($i = 0; $i < $maxAttempts; $i++) { $username = $this->credentialGenerator->generateUsername($event); $normalized = strtolower($username); $exists = EventPhotoboothSetting::query() ->where('username', $normalized) ->where('event_id', '!=', $event->getKey()) ->exists(); if (! $exists) { return $normalized; } } return strtolower('pb'.Str::random(5)); } protected function buildPath(Event $event): string { $tenantKey = $event->tenant?->slug ?? $event->tenant_id; return trim((string) $tenantKey, '/').'/'.$event->getKey(); } }