loadMissing('tenant'); return DB::transaction(function () use ($event, $settings) { $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); $event->forceFill([ 'photobooth_enabled' => true, 'photobooth_username' => $username, 'photobooth_password' => $password, 'photobooth_path' => $path, 'photobooth_expires_at' => $expiresAt, 'photobooth_status' => 'active', 'photobooth_last_provisioned_at' => now(), 'photobooth_metadata' => [ 'rate_limit_per_minute' => $settings->rate_limit_per_minute, ], ])->save(); return tap($event->refresh(), function (Event $refreshed) use ($password) { $refreshed->setAttribute('plain_photobooth_password', $password); }); }); } public function rotate(Event $event, ?PhotoboothSetting $settings = null): Event { $settings ??= PhotoboothSetting::current(); if (! $event->photobooth_enabled || ! $event->photobooth_username) { return $this->enable($event, $settings); } return DB::transaction(function () use ($event, $settings) { $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($event->photobooth_username, $payload, $settings); $event->forceFill([ 'photobooth_password' => $password, 'photobooth_expires_at' => $expiresAt, 'photobooth_status' => 'active', 'photobooth_last_provisioned_at' => now(), ])->save(); return tap($event->refresh(), function (Event $refreshed) use ($password) { $refreshed->setAttribute('plain_photobooth_password', $password); }); }); } public function disable(Event $event, ?PhotoboothSetting $settings = null): Event { if (! $event->photobooth_username) { return $event; } $settings ??= PhotoboothSetting::current(); return DB::transaction(function () use ($event, $settings) { try { $this->client->deleteUser($event->photobooth_username, $settings); } catch (\Throwable $exception) { Log::warning('Photobooth account deletion failed', [ 'event_id' => $event->id, 'username' => $event->photobooth_username, 'message' => $exception->getMessage(), ]); } $event->forceFill([ 'photobooth_enabled' => false, 'photobooth_status' => 'inactive', 'photobooth_username' => null, 'photobooth_password' => null, 'photobooth_path' => null, 'photobooth_expires_at' => null, 'photobooth_last_deprovisioned_at' => now(), ])->save(); return $event->refresh(); }); } 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); $exists = Event::query() ->where('photobooth_username', $username) ->whereKeyNot($event->getKey()) ->exists(); if (! $exists) { return strtolower($username); } } 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(); } }