From 0a6780d249e841e026536e0c06dc08c7e97b6a97fcc53ee7dcfc27cb57e82fde Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 9 Feb 2026 20:17:24 -0500 Subject: [PATCH] Add service credentials email notification on provisioning Sends mail notification with login credentials when VPS, dedicated, or web hosting services are provisioned. Wired into VirtFusion, SynergyCP, and Enhance provisioning services. Co-Authored-By: Claude Sonnet 4.5 --- .../ServiceCredentialsNotification.php | 58 ++++++++ .../Services/Provisioning/EnhanceService.php | 16 ++- .../Provisioning/SynergyCPService.php | 16 ++- .../Provisioning/VirtFusionService.php | 16 ++- .../ServiceCredentialsNotificationTest.php | 127 ++++++++++++++++++ 5 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 website/app/Notifications/ServiceCredentialsNotification.php create mode 100644 website/tests/Feature/ServiceCredentialsNotificationTest.php diff --git a/website/app/Notifications/ServiceCredentialsNotification.php b/website/app/Notifications/ServiceCredentialsNotification.php new file mode 100644 index 0000000..c46d9a6 --- /dev/null +++ b/website/app/Notifications/ServiceCredentialsNotification.php @@ -0,0 +1,58 @@ + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + $serviceType = ucfirst(str_replace('_', ' ', $this->service->service_type ?? 'Server')); + $planName = $this->service->plan?->name ?? $serviceType; + + $mail = (new MailMessage) + ->subject("Your {$serviceType} Server Credentials - EZSCALE") + ->greeting("Hello {$notifiable->name}!") + ->line("Your **{$planName}** service has been provisioned and is ready to use.") + ->line('Here are your access credentials:') + ->line("**Hostname:** {$this->credentials['hostname']}") + ->line("**IP Address:** {$this->credentials['ip_address']}") + ->line("**Username:** {$this->credentials['username']}") + ->line("**Password:** {$this->credentials['password']}"); + + if (! empty($this->credentials['port'])) { + $mail->line("**Port:** {$this->credentials['port']}"); + } + + if (! empty($this->credentials['panel_url'])) { + $mail->action('Access Control Panel', $this->credentials['panel_url']); + } + + return $mail + ->line('For security, we recommend changing your password after first login.') + ->line('If you need help, our support team is available 24/7.'); + } +} diff --git a/website/app/Services/Provisioning/EnhanceService.php b/website/app/Services/Provisioning/EnhanceService.php index f258eca..d50ba1c 100644 --- a/website/app/Services/Provisioning/EnhanceService.php +++ b/website/app/Services/Provisioning/EnhanceService.php @@ -6,6 +6,7 @@ namespace App\Services\Provisioning; use App\Models\ProvisioningLog; use App\Models\Service; +use App\Notifications\ServiceCredentialsNotification; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -70,7 +71,20 @@ class EnhanceService implements ProvisioningServiceInterface $this->logAction($service, 'provision', 'success', $data); - return $service->fresh(); + $service = $service->fresh(); + + if ($service->user) { + $service->user->notify(new ServiceCredentialsNotification($service, [ + 'username' => $data['data']['username'] ?? $user->email, + 'password' => $data['data']['password'] ?? 'see control panel', + 'hostname' => $service->domain ?? $service->ipv4_address ?? 'N/A', + 'ip_address' => $service->ipv4_address ?? 'Pending', + 'port' => null, + 'panel_url' => $data['data']['control_panel_url'] ?? null, + ])); + } + + return $service; } catch (RuntimeException $e) { throw $e; } catch (\Exception $e) { diff --git a/website/app/Services/Provisioning/SynergyCPService.php b/website/app/Services/Provisioning/SynergyCPService.php index a733ad3..fb25505 100644 --- a/website/app/Services/Provisioning/SynergyCPService.php +++ b/website/app/Services/Provisioning/SynergyCPService.php @@ -6,6 +6,7 @@ namespace App\Services\Provisioning; use App\Models\ProvisioningLog; use App\Models\Service; +use App\Notifications\ServiceCredentialsNotification; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -70,7 +71,20 @@ class SynergyCPService implements ProvisioningServiceInterface $this->logAction($service, 'provision', 'success', $data); - return $service->fresh(); + $service = $service->fresh(); + + if ($service->user) { + $service->user->notify(new ServiceCredentialsNotification($service, [ + 'username' => $data['data']['username'] ?? 'root', + 'password' => $data['data']['password'] ?? 'see control panel', + 'hostname' => $service->hostname ?? $service->ipv4_address ?? 'N/A', + 'ip_address' => $service->ipv4_address ?? 'Pending', + 'port' => $data['data']['ssh_port'] ?? 22, + 'panel_url' => $data['data']['ipmi_url'] ?? null, + ])); + } + + return $service; } catch (RuntimeException $e) { throw $e; } catch (\Exception $e) { diff --git a/website/app/Services/Provisioning/VirtFusionService.php b/website/app/Services/Provisioning/VirtFusionService.php index c17c0a4..656cf93 100644 --- a/website/app/Services/Provisioning/VirtFusionService.php +++ b/website/app/Services/Provisioning/VirtFusionService.php @@ -6,6 +6,7 @@ namespace App\Services\Provisioning; use App\Models\ProvisioningLog; use App\Models\Service; +use App\Notifications\ServiceCredentialsNotification; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -70,7 +71,20 @@ class VirtFusionService implements ProvisioningServiceInterface $this->logAction($service, 'provision', 'success', $data); - return $service->fresh(); + $service = $service->fresh(); + + if ($service->user) { + $service->user->notify(new ServiceCredentialsNotification($service, [ + 'username' => $data['data']['username'] ?? 'root', + 'password' => $data['data']['password'] ?? 'see control panel', + 'hostname' => $service->hostname ?? $service->ipv4_address ?? 'N/A', + 'ip_address' => $service->ipv4_address ?? 'Pending', + 'port' => $data['data']['ssh_port'] ?? 22, + 'panel_url' => $data['data']['vnc_url'] ?? null, + ])); + } + + return $service; } catch (RuntimeException $e) { throw $e; } catch (\Exception $e) { diff --git a/website/tests/Feature/ServiceCredentialsNotificationTest.php b/website/tests/Feature/ServiceCredentialsNotificationTest.php new file mode 100644 index 0000000..968f5cb --- /dev/null +++ b/website/tests/Feature/ServiceCredentialsNotificationTest.php @@ -0,0 +1,127 @@ +user = User::factory()->create(['name' => 'John Doe']); + $this->service = Service::factory()->create([ + 'user_id' => $this->user->id, + 'service_type' => 'vps', + 'hostname' => 'vps1.ezscale.cloud', + 'ipv4_address' => '192.168.1.100', + ]); +}); + +test('notification renders with all fields including port and panel url', function () { + $credentials = [ + 'username' => 'root', + 'password' => 's3cur3P@ss', + 'hostname' => 'vps1.ezscale.cloud', + 'ip_address' => '192.168.1.100', + 'port' => 22, + 'panel_url' => 'https://panel.ezscale.cloud/server/123', + ]; + + $notification = new ServiceCredentialsNotification($this->service, $credentials); + $mail = $notification->toMail($this->user); + + expect($mail->greeting)->toBe('Hello John Doe!') + ->and($mail->introLines)->toContain('Here are your access credentials:') + ->and($mail->introLines)->toContain('**Hostname:** vps1.ezscale.cloud') + ->and($mail->introLines)->toContain('**IP Address:** 192.168.1.100') + ->and($mail->introLines)->toContain('**Username:** root') + ->and($mail->introLines)->toContain('**Password:** s3cur3P@ss') + ->and($mail->introLines)->toContain('**Port:** 22') + ->and($mail->actionText)->toBe('Access Control Panel') + ->and($mail->actionUrl)->toBe('https://panel.ezscale.cloud/server/123') + ->and($mail->outroLines)->toContain('For security, we recommend changing your password after first login.') + ->and($mail->outroLines)->toContain('If you need help, our support team is available 24/7.'); +}); + +test('notification renders without optional fields', function () { + $credentials = [ + 'username' => 'root', + 'password' => 's3cur3P@ss', + 'hostname' => 'dedicated1.ezscale.cloud', + 'ip_address' => '10.0.0.50', + 'port' => null, + 'panel_url' => null, + ]; + + $service = Service::factory()->create([ + 'user_id' => $this->user->id, + 'service_type' => 'dedicated', + ]); + + $notification = new ServiceCredentialsNotification($service, $credentials); + $mail = $notification->toMail($this->user); + + // Port line should not be present + $allLines = array_merge($mail->introLines, $mail->outroLines); + $portLines = array_filter($allLines, fn (string $line): bool => str_contains($line, '**Port:**')); + expect($portLines)->toBeEmpty(); + + // No action button + expect($mail->actionText)->toBeNull() + ->and($mail->actionUrl)->toBeNull(); +}); + +test('notification uses mail channel only', function () { + $credentials = [ + 'username' => 'root', + 'password' => 'test123', + 'hostname' => 'server.ezscale.cloud', + 'ip_address' => '10.0.0.1', + 'port' => null, + 'panel_url' => null, + ]; + + $notification = new ServiceCredentialsNotification($this->service, $credentials); + + expect($notification->via($this->user))->toBe(['mail']); +}); + +test('notification subject includes service type', function () { + $credentials = [ + 'username' => 'root', + 'password' => 'test123', + 'hostname' => 'server.ezscale.cloud', + 'ip_address' => '10.0.0.1', + 'port' => null, + 'panel_url' => null, + ]; + + // Test VPS service type + $notification = new ServiceCredentialsNotification($this->service, $credentials); + $mail = $notification->toMail($this->user); + expect($mail->subject)->toBe('Your Vps Server Credentials - EZSCALE'); + + // Test dedicated service type + $dedicatedService = Service::factory()->create([ + 'user_id' => $this->user->id, + 'service_type' => 'dedicated', + ]); + $notification = new ServiceCredentialsNotification($dedicatedService, $credentials); + $mail = $notification->toMail($this->user); + expect($mail->subject)->toBe('Your Dedicated Server Credentials - EZSCALE'); + + // Test hosting service type + $hostingService = Service::factory()->create([ + 'user_id' => $this->user->id, + 'service_type' => 'hosting', + ]); + $notification = new ServiceCredentialsNotification($hostingService, $credentials); + $mail = $notification->toMail($this->user); + expect($mail->subject)->toBe('Your Hosting Server Credentials - EZSCALE'); + + // Test game_server service type (with underscore) + $gameService = Service::factory()->create([ + 'user_id' => $this->user->id, + 'service_type' => 'game_server', + ]); + $notification = new ServiceCredentialsNotification($gameService, $credentials); + $mail = $notification->toMail($this->user); + expect($mail->subject)->toBe('Your Game server Server Credentials - EZSCALE'); +});