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 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-09 20:17:24 -05:00
parent 76c49e9ed7
commit 0a6780d249
5 changed files with 230 additions and 3 deletions

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Notifications;
use App\Models\Service;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ServiceCredentialsNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* @param array{username: string, password: string, hostname: string, ip_address: string, port: int|null, panel_url: string|null} $credentials
*/
public function __construct(
public Service $service,
public array $credentials,
) {}
/** @return array<int, string> */
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.');
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -0,0 +1,127 @@
<?php
use App\Models\Service;
use App\Models\User;
use App\Notifications\ServiceCredentialsNotification;
beforeEach(function () {
$this->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');
});