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'); +});