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