Files
portal-api/app/Services/Plesk/PleskClientService.php
baodev0andClaude Sonnet 5.5 b44a88bb52 feat(service): huỷ dịch vụ, SSL, đổi mật khẩu hosting, lịch sử thanh toán theo hình thức
- 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>
2026-10-28 16:40:24 +07:00

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)]
);
}
}