feat: Implement Plesk domain and hosting services
- Added PleskDomainService for managing domain operations (create, update, remove, get info). - Introduced PleskHostingService for provisioning hosting accounts and notifying customers. - Created PleskService for handling API requests to Plesk. - Added migration to include 'type' column in dns_manager_servers table. - Created migration for module_hosting_accounts table to track hosting subscriptions. - Created migration for module_hosting_plesk_clients table to manage Plesk client credentials. - Added email templates for notifying customers and admins about hosting account creation.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Plesk;
|
||||
|
||||
use App\Http\Facade\HelperFacade as Helper;
|
||||
use App\Models\Customer;
|
||||
use App\Models\DnsServer;
|
||||
use App\Models\ModuleHostingPleskClient;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Plesk `/clients` entry point — the customer's own Plesk Panel login,
|
||||
* separate from the FTP/hosting credential PleskDomainService hands out.
|
||||
* Plesk never returns a client's password after creation, so it's stored
|
||||
* (encrypted) in module_hosting_plesk_clients — the only way to ever show
|
||||
* it again on reuse.
|
||||
*/
|
||||
class PleskClientService
|
||||
{
|
||||
public function __construct(private PleskService $plesk)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{error: bool, data?: array{id: int, login: string, password: ?string}, status?: int, message?: string}
|
||||
*/
|
||||
public function findOrCreateCustomer(DnsServer $server, Customer $customer): array
|
||||
{
|
||||
$existing = ModuleHostingPleskClient::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('dns_manager_server_id', $server->id)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return [
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'id' => $existing->plesk_client_id,
|
||||
'login' => $existing->login,
|
||||
'password' => Crypt::decryptString($existing->password),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$login = $this->generateUsername($customer);
|
||||
$password = Helper::general_password();
|
||||
|
||||
$created = $this->createCustomer($server, $customer, $login, $password);
|
||||
|
||||
if (!$created['error']) {
|
||||
$this->persist($customer, $server, $created['data']['id'], $login, $password);
|
||||
|
||||
return ['error' => false, 'data' => ['id' => $created['data']['id'], 'login' => $login, 'password' => $password]];
|
||||
}
|
||||
|
||||
// login already exists on this Plesk box but we have no local record of it (created
|
||||
// out-of-band, or our own table was reset) — Plesk never gives back the old password,
|
||||
// so reset it to one we generate now and persist that, instead of losing it again.
|
||||
if (in_array($created['status'] ?? null, [400, 409], true)) {
|
||||
$existingRemote = $this->findByLogin($server, $login);
|
||||
|
||||
if ($existingRemote) {
|
||||
$reset = $this->resetPassword($server, (int) $existingRemote['id'], $password);
|
||||
|
||||
if ($reset) {
|
||||
$this->persist($customer, $server, (int) $existingRemote['id'], $login, $password);
|
||||
|
||||
return ['error' => false, 'data' => ['id' => (int) $existingRemote['id'], 'login' => $login, 'password' => $password]];
|
||||
}
|
||||
|
||||
Log::error('Plesk client found but password reset failed — reusing without a known password', [
|
||||
'plesk_client_id' => $existingRemote['id'],
|
||||
'login' => $login,
|
||||
]);
|
||||
|
||||
return ['error' => false, 'data' => ['id' => (int) $existingRemote['id'], 'login' => $login, 'password' => null]];
|
||||
}
|
||||
}
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
private function createCustomer(DnsServer $server, Customer $customer, string $login, string $password): array
|
||||
{
|
||||
$param = [
|
||||
'name' => $customer->fullname ?: $login,
|
||||
'login' => $login,
|
||||
'email' => $customer->email,
|
||||
'password' => $password,
|
||||
'type' => 'customer',
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $this->plesk->client($server, 30)
|
||||
->post($this->plesk->url($server, 'clients'), $param);
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if ($response->successful() && isset($data['id'], $data['guid'])) {
|
||||
return ['error' => false, 'data' => $data];
|
||||
}
|
||||
|
||||
Log::error('Plesk client creation failed', ['status' => $response->status(), 'response' => $data]);
|
||||
|
||||
return [
|
||||
'error' => true,
|
||||
'status' => $response->status(),
|
||||
'message' => $data['message'] ?? 'Plesk client creation failed',
|
||||
];
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk client creation failed', ['error' => $e->getMessage(), 'response' => $e->response?->body()]);
|
||||
|
||||
return ['error' => true, 'status' => $e->response?->status(), 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPassword(DnsServer $server, int $pleskClientId, string $password): bool
|
||||
{
|
||||
try {
|
||||
$response = $this->plesk->client($server, 15)
|
||||
->put($this->plesk->url($server, 'clients', "/{$pleskClientId}"), ['password' => $password]);
|
||||
|
||||
return $response->successful();
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk client password reset failed', ['error' => $e->getMessage(), 'plesk_client_id' => $pleskClientId]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function findByLogin(DnsServer $server, string $login): ?array
|
||||
{
|
||||
try {
|
||||
$response = $this->plesk->client($server, 15)->get($this->plesk->url($server, 'clients'));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($response->json() ?? [] as $client) {
|
||||
if (($client['login'] ?? null) === $login) {
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plesk `login` is a short username, not the email (email is its own field
|
||||
* on the same payload). Derived from the email's local part + customer id
|
||||
* so it's stable and never collides across customers — needed because a
|
||||
* 409 on create is treated as "this customer already has a client".
|
||||
*/
|
||||
private function generateUsername(Customer $customer): string
|
||||
{
|
||||
$localPart = strtolower((string) strstr($customer->email, '@', true) ?: 'user');
|
||||
$sanitized = trim((string) preg_replace('/[^a-z0-9]+/', '_', $localPart), '_') ?: 'user';
|
||||
|
||||
return substr($sanitized, 0, 32) . '_' . $customer->id;
|
||||
}
|
||||
|
||||
private function persist(Customer $customer, DnsServer $server, int $pleskClientId, string $login, string $password): void
|
||||
{
|
||||
ModuleHostingPleskClient::query()->updateOrCreate(
|
||||
['customer_id' => $customer->id, 'dns_manager_server_id' => $server->id],
|
||||
['plesk_client_id' => $pleskClientId, 'login' => $login, 'password' => Crypt::encryptString($password)]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user