- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú - `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách - Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý - Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
214 lines
8.1 KiB
PHP
214 lines
8.1 KiB
PHP
<?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) {
|
|
if ($this->clientExists($server, (int) $existing->plesk_client_id)) {
|
|
return [
|
|
'error' => false,
|
|
'data' => [
|
|
'id' => $existing->plesk_client_id,
|
|
'login' => $existing->login,
|
|
'password' => Crypt::decryptString($existing->password),
|
|
],
|
|
];
|
|
}
|
|
|
|
// Cached client was deleted directly on Plesk (out-of-band) — the id is
|
|
// dead, so any call reusing it (e.g. owner_client on domain creation)
|
|
// fails with "Owner does not exist". Drop the stale row and re-provision.
|
|
Log::warning('Cached Plesk client no longer exists on server — re-creating', [
|
|
'plesk_client_id' => $existing->plesk_client_id,
|
|
'customer_id' => $customer->id,
|
|
'server_id' => $server->id,
|
|
]);
|
|
|
|
$existing->delete();
|
|
}
|
|
|
|
$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()];
|
|
}
|
|
}
|
|
|
|
public function changePassword(DnsServer $server, ModuleHostingPleskClient $client, string $password): bool
|
|
{
|
|
if (!$this->resetPassword($server, (int) $client->plesk_client_id, $password)) {
|
|
return false;
|
|
}
|
|
|
|
$client->update(['password' => Crypt::encryptString($password)]);
|
|
|
|
return true;
|
|
}
|
|
|
|
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 clientExists(DnsServer $server, int $pleskClientId): bool
|
|
{
|
|
try {
|
|
$response = $this->plesk->client($server, 15)
|
|
->get($this->plesk->url($server, 'clients', "/{$pleskClientId}"));
|
|
|
|
return $response->successful();
|
|
} catch (RequestException $e) {
|
|
Log::error('Plesk client existence check 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)]
|
|
);
|
|
}
|
|
}
|