From 79f06c2fa24baca5d97b7a6a2e1bac8e06a34ae1 Mon Sep 17 00:00:00 2001 From: baodev0 Date: Thu, 6 Aug 2026 15:32:51 +0700 Subject: [PATCH] Add bulk update gateway endpoint to vCenter API routes - Introduced a new POST route 'bulk-update-gateway' for bulk updating gateways. - This route is protected by the 'module-vcenter.manager' permission middleware. --- .claude/settings.local.json | 6 +- .../Api/Admin/IpPool/IpPoolController.php | 108 +- app/Imports/IpPoolImport.php | 57 +- app/Models/IpPool.php | 71 +- app/Models/IpPoolContact.php | 15 + ...6_120421_create_ip_pool_contacts_table.php | 26 + docs/hosting-module-design.md | 582 ++ docs/open-api-plesk.md | 4708 +++++++++++++++++ routes/api.php | 1 + 9 files changed, 5555 insertions(+), 19 deletions(-) create mode 100644 app/Models/IpPoolContact.php create mode 100644 database/migrations/2026_08_06_120421_create_ip_pool_contacts_table.php create mode 100644 docs/hosting-module-design.md create mode 100644 docs/open-api-plesk.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f8723f2..8c32391 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -40,7 +40,11 @@ "Bash(grep -n -A 30 \"CREATE TABLE \\\\`products\\\\`\" /Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal.sql)", "Bash(grep -n -A 30 \"CREATE TABLE \\\\`vmware\\\\`\" /Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal.sql)", "Bash(pnpm tsc *)", - "Bash(grep *)" + "Bash(grep *)", + "Bash(python3 *)", + "Bash(pnpm exec *)", + "Bash(echo \"exit: $?\")", + "Bash(node -e \"console.log\\(require.resolve\\('ag-grid-react/package.json'\\)\\)\")" ] } } diff --git a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php index 41df81a..cc399da 100644 --- a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php +++ b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php @@ -15,8 +15,11 @@ use Maatwebsite\Excel\Validators\ValidationException; class IpPoolController extends Controller { + /** + * Descriptive/CSV-import fields — plain pass-through, unlike gateway/cidr/netmask/ + * status/service_id which have defaulting + module_ip_address mirror logic below. + */ private const EDITABLE_FIELDS = [ - 'gateway', 'netmask', 'service_id', 'status', 'owner_type', 'ip_role', 'parent_ip', 'customer_name', 'service_name', 'allocated_at', 'revoked_at', 'allocation_status', 'contact_email', 'contact_cc', 'contact_phone', @@ -24,9 +27,25 @@ class IpPoolController extends Controller 'last_email_type', 'last_email_at', 'updated_by', 'note', ]; + /** + * contact_email/contact_cc may hold multiple addresses separated by comma/semicolon. + */ + private function multiEmailRule(): \Closure + { + return function (string $attribute, mixed $value, \Closure $fail) { + foreach (preg_split('/[,;]+/', (string) $value) as $email) { + $email = trim($email); + if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + $fail("Email không hợp lệ: {$email}"); + } + } + }; + } + public function index(Request $request): JsonResponse { - $perPage = min((int) $request->get('per_page', 15), 500); + // 'all' lets the admin grid load the full dataset once and filter/sort client-side. + $perPage = $request->boolean('all') ? 20000 : min((int) $request->get('per_page', 15), 500); $filter = $request->get('filter'); $networkId = $request->get('network_id'); @@ -133,6 +152,10 @@ class IpPoolController extends Controller 'netmask' => 'nullable|string|max:45', 'status' => 'nullable|in:free,assigned,reserved', 'service_id' => 'nullable|integer|exists:services,id', + 'contact_email' => ['nullable', 'string', $this->multiEmailRule()], + 'contact_cc' => ['nullable', 'string', $this->multiEmailRule()], + 'monthly_cost' => 'nullable|numeric', + 'selling_price' => 'nullable|numeric', ]); $networkId = (int) $request->module_network_id; @@ -174,6 +197,7 @@ class IpPoolController extends Controller $hostId = ModuleNetwork::find($networkId)?->module_host_id; $netmask = $request->netmask ?: '255.255.255.0'; $cidr = $request->cidr ?? 24; + $extra = $request->only(self::EDITABLE_FIELDS); DB::beginTransaction(); try { @@ -187,6 +211,7 @@ class IpPoolController extends Controller 'netmask' => $netmask, 'status' => $request->status ?: 'free', 'service_id' => $request->service_id, + ...$extra, ]); // Mirror into module_ip_address so both tables stay in sync. @@ -229,6 +254,10 @@ class IpPoolController extends Controller 'netmask' => 'nullable|string|max:45', 'status' => 'nullable|in:free,assigned,reserved', 'service_id' => 'nullable|integer|exists:services,id', + 'contact_email' => ['nullable', 'string', $this->multiEmailRule()], + 'contact_cc' => ['nullable', 'string', $this->multiEmailRule()], + 'monthly_cost' => 'nullable|numeric', + 'selling_price' => 'nullable|numeric', ]); if (!filter_var($request->ip_address, FILTER_VALIDATE_IP)) { @@ -256,6 +285,7 @@ class IpPoolController extends Controller try { $pool->update([ ...$request->only(['ip_address', 'module_network_id', 'gateway', 'cidr', 'service_id']), + ...$request->only(self::EDITABLE_FIELDS), 'netmask' => $netmask, 'status' => $request->status ?: $pool->status, ]); @@ -312,6 +342,44 @@ class IpPoolController extends Controller return response()->json(['error' => false, 'message' => 'Đã xoá IP Pool thành công']); } + public function bulkUpdateGateway(Request $request): JsonResponse + { + $request->validate([ + 'ids' => 'required|array|min:1', + 'ids.*' => 'integer', + 'gateway' => 'required|string|max:45', + ]); + + $pools = IpPool::whereIn('id', $request->ids)->get(['id', 'ip_address', 'module_network_id']); + + if ($pools->isEmpty()) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy IP Pool nào'], 404); + } + + DB::beginTransaction(); + try { + IpPool::whereIn('id', $pools->pluck('id'))->update(['gateway' => $request->gateway]); + + // Mirror into module_ip_address, grouped by network to keep this to a handful of queries. + foreach ($pools->groupBy('module_network_id') as $networkId => $group) { + ModuleIpAddress::whereIn('ip', $group->pluck('ip_address')) + ->where('module_network_id', $networkId) + ->update(['gateway' => $request->gateway]); + } + + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['error' => true, 'message' => $e->getMessage()], 500); + } + + return response()->json([ + 'error' => false, + 'message' => "Đã cập nhật gateway cho {$pools->count()} IP", + 'updated' => $pools->count(), + ]); + } + public function bulkDelete(Request $request): JsonResponse { $request->validate([ @@ -354,6 +422,42 @@ class IpPoolController extends Controller ]); } + public function import(Request $request): JsonResponse + { + $request->validate([ + 'file' => 'required|file|mimes:csv,txt|max:10240', + ]); + + $countBefore = IpPool::withTrashed()->count(); + + try { + Excel::import(new IpPoolImport(), $request->file('file')); + } catch (ValidationException $e) { + $errors = []; + foreach ($e->failures() as $failure) { + $errors[] = 'Dòng ' . $failure->row() . ': ' . implode(', ', $failure->errors()); + } + + return response()->json([ + 'error' => true, + 'message' => 'Import thất bại', + 'errors' => $errors, + ], 422); + } catch (\Exception $e) { + return response()->json(['error' => true, 'message' => 'Import thất bại: ' . $e->getMessage()], 500); + } + + $countAfter = IpPool::withTrashed()->count(); + + IpPool::resyncAllContacts(); + + return response()->json([ + 'error' => false, + 'message' => 'Import thành công', + 'created' => max(0, $countAfter - $countBefore), + ]); + } + /** * Backfill/repair the module_ip_address mirror for every ip_pools row. * Existing mirror rows only get gateway/netmask refreshed — status/vm_name diff --git a/app/Imports/IpPoolImport.php b/app/Imports/IpPoolImport.php index 154365d..50f80c9 100644 --- a/app/Imports/IpPoolImport.php +++ b/app/Imports/IpPoolImport.php @@ -4,6 +4,7 @@ namespace App\Imports; use App\Models\IpPool; use App\Models\ModuleNetwork; +use Carbon\Carbon; use Illuminate\Support\Str; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithHeadingRow; @@ -13,17 +14,22 @@ use Maatwebsite\Excel\Concerns\WithValidation; class IpPoolImport implements ToModel, WithHeadingRow, WithValidation, WithUpserts, WithUpsertColumns { - /** @var array */ + /** @var array */ private array $networkCache = []; public function model(array $row) { + $network = $this->resolveNetwork($row['ten_mang'] ?? null, $row['cidr'] ?? null); + return new IpPool([ // uuid has no DB default and must be set on insert; excluded from upsertColumns() so it's never overwritten 'uuid' => (string) Str::uuid(), 'ip_address' => $row['ip_address'], - 'module_network_id' => $this->resolveNetworkId($row['ten_mang'] ?? null, $row['cidr'] ?? null), - 'cidr' => $this->parsePrefixLength($row['cidr'] ?? null), + 'module_network_id' => $network?->id, + // ip_pools.cidr is NOT NULL; fall back to the network's own prefix (or 24) when the row omits it + 'cidr' => $this->parsePrefixLength($row['cidr'] ?? null) + ?? $this->parsePrefixLength($network?->network) + ?? 24, 'gateway' => $row['gateway'] ?? null, 'netmask' => $row['subnet_mask'] ?? null, 'owner_type' => $row['owner_type'] ?? null, @@ -31,8 +37,8 @@ class IpPoolImport implements ToModel, WithHeadingRow, WithValidation, WithUpser 'parent_ip' => $row['parent_ip'] ?? null, 'customer_name' => $row['ten_khach_hang'] ?? null, 'service_name' => $row['ten_dich_vu'] ?? null, - 'allocated_at' => $row['ngay_cap_phat'] ?: null, - 'revoked_at' => $row['ngay_thu_hoi'] ?: null, + 'allocated_at' => $this->parseDate($row['ngay_cap_phat'] ?? null), + 'revoked_at' => $this->parseDate($row['ngay_thu_hoi'] ?? null), 'allocation_status' => $row['trang_thai'] ?? null, 'contact_email' => $row['contact_email'] ?? null, 'contact_cc' => $row['contact_cc'] ?? null, @@ -41,7 +47,7 @@ class IpPoolImport implements ToModel, WithHeadingRow, WithValidation, WithUpser 'selling_price' => $row['selling_price'] ?: null, 'additional_info' => $row['thong_tin_bo_sung'] ?? null, 'last_email_type' => $row['last_email_type'] ?? null, - 'last_email_at' => $row['last_email_at'] ?: null, + 'last_email_at' => $this->parseDate($row['last_email_at'] ?? null), 'updated_by' => $row['updated_by'] ?? null, 'note' => $row['ghi_chu'] ?? null, ]); @@ -49,16 +55,49 @@ class IpPoolImport implements ToModel, WithHeadingRow, WithValidation, WithUpser public function rules(): array { + $multiEmail = function (string $attribute, mixed $value, \Closure $fail) { + foreach (preg_split('/[,;]+/', (string) $value) as $email) { + $email = trim($email); + if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + $fail("Email không hợp lệ: {$email}"); + } + } + }; + return [ 'ten_mang' => 'required|string|max:255', 'cidr' => 'nullable|string|max:45', 'ip_address' => 'required|ip', - 'contact_email' => 'nullable|email', + 'contact_email' => ['nullable', 'string', $multiEmail], + 'contact_cc' => ['nullable', 'string', $multiEmail], 'monthly_cost' => 'nullable|numeric', 'selling_price' => 'nullable|numeric', ]; } + /** + * CSV dates come as "dd/mm/yyyy" or "dd/mm/yyyy H:i:s" (Vietnamese sheet export). + */ + private function parseDate(?string $value): ?string + { + $value = trim((string) $value); + if ($value === '') { + return null; + } + + try { + return Carbon::createFromFormat('d/m/Y H:i:s', $value)->toDateTimeString(); + } catch (\Throwable) { + // not a datetime, try date-only below + } + + try { + return Carbon::createFromFormat('d/m/Y', $value)->startOfDay()->toDateTimeString(); + } catch (\Throwable) { + return null; + } + } + /** * CSV gives CIDR as "/22"; the ip_pools.cidr column only stores the prefix length. */ @@ -83,7 +122,7 @@ class IpPoolImport implements ToModel, WithHeadingRow, WithValidation, WithUpser ]; } - private function resolveNetworkId(?string $name, ?string $cidr): ?int + private function resolveNetwork(?string $name, ?string $cidr): ?ModuleNetwork { if (!$name) { return null; @@ -99,6 +138,6 @@ class IpPoolImport implements ToModel, WithHeadingRow, WithValidation, WithUpser ?? ($cidr ? ModuleNetwork::where('network', $cidr)->first() : null) ?? ModuleNetwork::create(['name' => $name, 'network' => $cidr]); - return $this->networkCache[$key] = $network->id; + return $this->networkCache[$key] = $network; } } diff --git a/app/Models/IpPool.php b/app/Models/IpPool.php index fd16763..e3f9518 100644 --- a/app/Models/IpPool.php +++ b/app/Models/IpPool.php @@ -13,16 +13,12 @@ class IpPool extends Model use HasFactory, SoftDeletes, LogsActivity; protected $fillable = [ - 'uuid', 'uuid', 'ip_address', 'gateway', 'cidr', 'netmask', 'module_network_id', - 'gateway', - 'cidr', - 'netmask', 'status', 'owner_type', 'ip_role', @@ -32,6 +28,7 @@ class IpPool extends Model 'allocated_at', 'revoked_at', 'allocation_status', + 'service_id', 'contact_email', 'contact_cc', 'contact_phone', @@ -51,9 +48,6 @@ class IpPool extends Model 'monthly_cost' => 'decimal:2', 'selling_price' => 'decimal:2', ]; - 'status', - 'service_id', - ]; protected static function boot(): void { @@ -64,6 +58,14 @@ class IpPool extends Model $model->uuid = (string) Str::uuid(); } }); + + // Keep ip_pool_contacts in sync whenever contact_email/contact_cc change via normal Eloquent saves. + // Bulk import (WithUpserts) bypasses this — call resyncAllContacts() after Excel::import() instead. + static::saved(function (self $model) { + if ($model->wasChanged(['contact_email', 'contact_cc']) || $model->wasRecentlyCreated) { + $model->syncContacts(); + } + }); } public function network() @@ -80,4 +82,59 @@ class IpPool extends Model { return $this->hasMany(VirtualMachines::class, 'ip_pool_id'); } + + public function contacts() + { + return $this->hasMany(IpPoolContact::class); + } + + /** + * contact_email/contact_cc may hold multiple addresses separated by comma/semicolon + * (e.g. imported from Google Sheet). Split into a clean array, dropping invalid/empty entries. + */ + public static function splitEmails(?string $value): array + { + if (!$value) { + return []; + } + + return collect(preg_split('/[,;]+/', $value)) + ->map(fn ($e) => trim($e)) + ->filter(fn ($e) => $e !== '' && filter_var($e, FILTER_VALIDATE_EMAIL)) + ->unique() + ->values() + ->all(); + } + + public function syncContacts(): void + { + $rows = []; + foreach (self::splitEmails($this->contact_email) as $email) { + $rows[] = ['ip_pool_id' => $this->id, 'email' => $email, 'type' => 'to']; + } + foreach (self::splitEmails($this->contact_cc) as $email) { + $rows[] = ['ip_pool_id' => $this->id, 'email' => $email, 'type' => 'cc']; + } + + $this->contacts()->delete(); + if ($rows) { + IpPoolContact::insert($rows); + } + } + + /** + * Full resync for bulk paths (CSV import) that bypass the saved() hook above. + */ + public static function resyncAllContacts(): void + { + self::withTrashed() + ->where(function ($q) { + $q->whereNotNull('contact_email')->orWhereNotNull('contact_cc'); + }) + ->chunkById(500, function ($pools) { + foreach ($pools as $pool) { + $pool->syncContacts(); + } + }); + } } diff --git a/app/Models/IpPoolContact.php b/app/Models/IpPoolContact.php new file mode 100644 index 0000000..eeae797 --- /dev/null +++ b/app/Models/IpPoolContact.php @@ -0,0 +1,15 @@ +belongsTo(IpPool::class); + } +} diff --git a/database/migrations/2026_08_06_120421_create_ip_pool_contacts_table.php b/database/migrations/2026_08_06_120421_create_ip_pool_contacts_table.php new file mode 100644 index 0000000..b1a3e9d --- /dev/null +++ b/database/migrations/2026_08_06_120421_create_ip_pool_contacts_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('ip_pool_id')->constrained('ip_pools')->cascadeOnDelete(); + $table->string('email'); + $table->enum('type', ['to', 'cc']); + $table->timestamps(); + + $table->unique(['ip_pool_id', 'email', 'type']); + }); + } + + public function down(): void + { + Schema::dropIfExists('ip_pool_contacts'); + } +}; diff --git a/docs/hosting-module-design.md b/docs/hosting-module-design.md new file mode 100644 index 0000000..b163ccb --- /dev/null +++ b/docs/hosting-module-design.md @@ -0,0 +1,582 @@ +# Hosting Management Module — Phase 1 (Provisioning) Design + +Scope: **create Plesk Customer → create Hosting Subscription → assign Plan → +persist the local record.** Nothing else. Database/SSL/Mail/Git +deploy/Let's Encrypt/auto-upload are explicitly out of scope and are not +stubbed — no placeholder methods for them are created in this pass. + +This revises the earlier proposal in this file: `type` is now a `varchar` +column on `dns_manager_servers` (not a capability pivot), per explicit +instruction. Everything else below is re-verified against the actual +source, not carried over by assumption. + +## 1. Audit — what's actually in the codebase + +### 1.1 Domain / DNS / vCenter modules + +- **DNS**: table `dns_manager_servers` (model `App\Models\DnsServer`) — the + live Plesk credential store: `id, name, ip_address, username, password, + api_key, status, timestamps`. No `type`, `hostname`, `port`, `protocol` + columns exist today. `dns_manager_domain` (model `DnsDomain`) has + `dns_manager_server_id` FK and `plesk_id`. `dns_manager_ns` (model + `DnsManagerNS`) has `plesk_id` too. + Repository: `App\Repositories\DnsManager\Server\DnsServerRepositoryInterface` + → bound in `RepositoryProvider` — **actually check**: this specific + interface/binding was *not* found in `RepositoryProvider.php`'s bind list + (only `Module\DnsServer\DnsServerRepositoryInterface`, a different, + legacy class tied to table `module_dns_server`, was found bound). This + means `ServerService` (below) currently resolves + `DnsManager\Server\DnsServerRepositoryInterface` from the container + without an explicit binding visible in the audited file — either it's + bound elsewhere (a second provider) or Laravel's implicit binding/ + interface-to-concrete auto-resolution isn't in play and this is already + fragile. **Do not copy this uncertainty into the new module** — bind the + new interface explicitly (§6). +- Plesk HTTP calls today live in `App\Services\ServerService`, talking to + `:8443/api/v2/{server,domains,dns/records}` with + `Http::withoutVerifying()->withBasicAuth($username, Crypt::decryptString($password))`. + It already implements `createDomain()`, `updateDomain()`, `removeDomain()`, + `getDomainInfo()`, `listRecords()`, `checkConnection()`. `createDomain()` + already POSTs `hosting_settings.ftp_login/ftp_password` + `plan` — i.e. + it's already creating a **hosting-capable** Plesk domain object, not a + bare DNS zone. This is the piece to extract and reuse (§7), not + reimplement. +- **vCenter**: `App\Http\Services\vCenter\Client` — same shape (`Http` + facade, Basic Auth, returns raw arrays/bools), different domain. Confirms + "thin service class over Http facade, no HTTP abstraction layer" is the + house style, not a DNS-module one-off. +- **Legacy Hosting** (`module_hosting`, `module_hosting_server`, + `App\Http\Services\Hosting`, `App\Jobs\Hosting\CreateHostingJobs`): a + pre-`services` era, WHMCS-shaped flow. Different crypto (`SignRSA`, not + `Crypt::`), different credential table, different (non-Plesk-v2-REST) + API surface (`CheckUserClient`, `CreateNewClient`, `CreateNewDomain`, + `CreateExpirationForDomain`), stores the account password directly on + `module_hosting.password`. **Not reused** — instruction says no legacy + tables and no duplicated data; this codebase's own `services`/`products` + layer already superseded it for VPS (`module_vmware` + `services.vm_id`) + and Hosting should follow that same replacement, not the old one. +- **`control_panels` / `product_control_panel`**: exist, seeded (Plesk, + DirectAdmin, cPanel, ...), unused by any controller/service found. Out of + scope to wire up in Phase 1 (no product-selection UI requested), but + worth knowing they exist so Phase 1 doesn't hardcode "Plesk" somewhere + that later blocks reading the panel off the product. + +### 1.2 Repository Pattern + +Confirmed convention, three files per resource: + +``` +XRepositoryInterface extends BaseRepositoryInterface +XRepository extends BaseRepository (getModel(), query logic) +XCachedRepository extends BaseCachedRepository, implements XRepositoryInterface + (wraps XRepository, Cache::remember on read methods) +``` + +Bound in `app/Providers/RepositoryProvider.php`: +`$this->app->singleton(XRepositoryInterface::class, XCachedRepository::class);` + +This is the pattern used for `Module\DnsServer`, `Module\HostingServer`, +`Module\Hosting` (legacy), `Domain`, etc. — followed as-is for the new +module, no changes to the pattern itself. + +### 1.3 Service Pattern + +Plain `App\Services\*` or `App\Http\Services\{Domain}\*` classes, no +interfaces, no DI container binding beyond constructor injection of +repositories. Return shape for external-API-calling services observed +consistently: `['error' => bool, 'data' => ..., 'message' => ...]` (see +`ServerService` — every public method returns this shape or `array|false`). +Followed as-is. + +### 1.4 Naming convention + +- Tables: snake_case, no prefix for "core" entities (`customers`, + `services`, `products`), `module_` prefix for provisioned-resource + tables descended from the legacy admin panel (`module_vmware`, + `module_dns_server`, `module_hosting`). No `tbl_` prefix anywhere in the + codebase. +- Migrations: `unsignedBigInteger('x_id')->index()` + a separate + `$table->foreign('x_id')->references('id')->on('x')->...` call is the + dominant style (46 migrations) over the `foreignId()` shorthand (18 + migrations, mostly older). Followed as the majority style for + consistency with every table this module touches + (`dns_manager_servers`, `services`, `customers`). +- Migration filenames: `YYYY_MM_DD_HHMMSS_verb_noun.php`, one + `Schema::create`/`Schema::table` block per concern; multi-step column + changes get their own migration file rather than being folded into the + original create (e.g. `add_domain_fields_to_services.php`, + `alter_services_vm_id_to_fk.php`). + +### 1.5 Permission convention + +Seeded in `database/seeders/RolesAndPermissions.php`, pattern +`{module}.{view|create|edit|destroy|manager}`, guard `admin`. Routes apply +via `->middleware('perm:{module}.{action}')`. + +**A `module-hosting.{view,create,edit,destroy,manager}` permission set +already exists** in the seeder (tied to the legacy `module_hosting` +admin-blade screens, no route match found in `routes/api.php` — i.e. +currently orphaned or admin-blade-only). Reusing it for the new API would +silently couple this module's authorization to whatever the legacy screens +still do with it. New module gets its own permission group instead: +`hosting.{view,create,edit,destroy}` — matches the flat `domain.*`/ +`customer.*` style (no module- prefix) used for the *modern*, +`services`-era modules, as opposed to the `module-*` prefix used for the +older admin-panel-generation features (`module-vcenter.*`, +`module-dns.*`). This keeps "which permission style = which generation of +code" legible, which is already implicitly true in the seeder today. + +## 2. `dns_manager_servers.type` + +Decision: **`varchar`, not a native SQL `ENUM`.** + +Reasons, specific to this codebase: +- Zero existing migrations use MySQL `->enum(...)`. `dns_manager_servers.status` + itself is a `string` column with values validated by convention/PHP, + not a DB enum. Matching that means `type` should be `string` too — one + column-kind convention for the whole table, not a mix. +- `App\Enums\Domain\DomainProvisioningStatus` shows the actual "enum" + convention in this codebase: a PHP backed enum class used for validation + and `label()`/transition logic, with the DB column being a plain + `string`. Adding `both` as a third case later, or a `mail`/`cdn` value in + some future phase, is then a one-line enum case, not a migration to + widen a native `ENUM` definition (which MySQL handles as a full table + metadata rewrite). +- Pivot/capability table and JSON were explicitly excluded by the request; + they're also not needed here — `domain | hosting | both` is a small, + stable, mutually-exclusive-enough set for a single column, unlike the + earlier revision's "server can have an open-ended combination" framing. + +```php +enum ServerType: string +{ + case DOMAIN = 'domain'; + case HOSTING = 'hosting'; + case BOTH = 'both'; +} +``` + +Query helper needed because of `both`: any lookup for "servers usable for +hosting" must match `type IN ('hosting', 'both')`, not `type = 'hosting'`. +This is the one real cost of `varchar`+`both` over a pivot — every query +site needs to know to expand `both`. Put that in one place: +`DnsServerRepositoryInterface::getUsableFor(ServerType $type)` on the +repository, so `PleskHostingService` and any future admin filter never +hand-roll the `IN (...)` themselves. + +```php +// migration: add_type_to_dns_manager_servers.php +Schema::table('dns_manager_servers', function (Blueprint $table) { + $table->string('type', 20)->default('domain')->after('status'); +}); +// backfill: every existing row already serves DNS today +DB::table('dns_manager_servers')->update(['type' => 'domain']); +``` + +## 3. Hosting module — table name + +`module_hosting_accounts`. Follows the `module_` + resource-noun pattern +already used for `module_vmware` (the closest analog: a provisioned +resource hanging off `services`, one credential-holding parent per row). +Does not collide with legacy `module_hosting`/`module_hosting_server` +(different tables, different meaning, left untouched — not migrated into, +not read from). + +## 4. Migration + +```php +// database/migrations/2026_08_06_130000_add_type_to_dns_manager_servers_table.php +Schema::table('dns_manager_servers', function (Blueprint $table) { + $table->string('type', 20)->default('domain')->after('status'); +}); +``` + +```php +// database/migrations/2026_08_06_130001_create_module_hosting_accounts_table.php +Schema::create('module_hosting_accounts', function (Blueprint $table) { + $table->id(); + + $table->unsignedBigInteger('customer_id')->index(); + $table->unsignedBigInteger('service_id')->nullable()->index(); + $table->unsignedBigInteger('dns_manager_server_id')->index(); + $table->unsignedBigInteger('dns_manager_domain_id')->nullable()->index(); + + $table->unsignedInteger('plesk_client_id')->nullable(); + $table->unsignedInteger('plesk_subscription_id')->nullable()->index(); + $table->string('plesk_guid', 64)->nullable(); + + $table->string('hosting_plan', 100)->nullable(); + $table->string('ftp_username', 100)->nullable(); + $table->string('system_username', 100)->nullable(); + $table->string('document_root', 255)->nullable(); + + // pending -> active -> suspended -> terminated (mirrors services.status vocabulary) + $table->string('status', 20)->default('pending')->index(); + + $table->timestamps(); + + $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade'); + $table->foreign('service_id')->references('id')->on('services')->onDelete('set null'); + $table->foreign('dns_manager_server_id')->references('id')->on('dns_manager_servers')->onDelete('restrict'); + $table->foreign('dns_manager_domain_id')->references('id')->on('dns_manager_domain')->onDelete('set null'); +}); +``` + +No password / FTP-password / Basic-Auth column — confirmed not needed: +`dns_manager_servers` already holds what's required to manage the box: the +FTP account Plesk creates is reachable/resettable through the same +Basic-Auth credential, so nothing hosting-account-specific needs storing +locally. (`module_vmware.plain_password` is a known wart elsewhere in this +codebase — not repeated here.) + +`plesk_guid` is `string(64)`, not `uuid()`: Plesk's GUID format isn't +guaranteed to be a canonical RFC-4122 UUID the DB driver's `uuid` column +type would validate against; storing it as an opaque string avoids a +runtime insert failure on a format mismatch. + +`service_id` is nullable: a hosting account can be provisioned by an admin +ahead of/independent from an order line in Phase 1 (no order/service flow +is being built here), matching how `services.vm_id` is nullable during +the VM's own pending window. + +## 5. Model + +```php +// app/Models/ModuleHostingAccount.php +namespace App\Models; + +use App\Traits\LogsActivity; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; + +class ModuleHostingAccount extends Model +{ + use HasFactory, LogsActivity; + + protected $table = 'module_hosting_accounts'; + + protected $fillable = [ + 'customer_id', + 'service_id', + 'dns_manager_server_id', + 'dns_manager_domain_id', + 'plesk_client_id', + 'plesk_subscription_id', + 'plesk_guid', + 'hosting_plan', + 'ftp_username', + 'system_username', + 'document_root', + 'status', + ]; + + public function customer() + { + return $this->belongsTo(Customer::class); + } + + public function service() + { + return $this->belongsTo(Service::class); + } + + public function server() + { + return $this->belongsTo(DnsServer::class, 'dns_manager_server_id'); + } + + public function domain() + { + return $this->belongsTo(DnsDomain::class, 'dns_manager_domain_id'); + } +} +``` + +`DnsServer` gets one addition — a typed accessor for `type`, matching how +`DnsDomain`/`DnsServer` already expose small helper methods +(`getStatus()`): + +```php +// App\Models\DnsServer — add +public function servesHosting(): bool +{ + return in_array($this->attributes['type'] ?? 'domain', ['hosting', 'both'], true); +} +``` + +## 6. Relationships + +``` +customers 1──* module_hosting_accounts +services 1──0..1 module_hosting_accounts (service_id, nullable — same shape as services.vm_id → module_vmware) +dns_manager_servers 1──* module_hosting_accounts (type must include 'hosting' or 'both') +dns_manager_domain 1──0..1 module_hosting_accounts (dns_manager_domain_id, nullable) +``` + +``` +Customer + └─ Hosting Account (module_hosting_accounts) + ├─ DNS Manager Server (dns_manager_servers, type ∈ {hosting, both}) + └─ DNS Domain (dns_manager_domain, optional) +``` + +**Domain Module link — yes, loosely, and only via `dns_manager_domain`, +not `App\Models\Domain`.** Reasoning: + +- `App\Models\Domain` (registrar-side TLD ownership, the "Domain + Management" module in the request) and `App\Models\DnsDomain` + (`dns_manager_domain`, a DNS zone actually hosted on one of our Plesk + boxes) are different entities in the current schema — a customer can own + a registrar domain that has never been pointed at our DNS/Plesk + infrastructure, and conversely a DNS zone here doesn't strictly need to + correspond to a domain we sold. Nothing in the audited code links them + 1:1 today. +- Hosting only needs "which DNS zone/vhost name will Plesk serve this + subscription under," which is exactly what `dns_manager_domain_id` + captures. Forcing a link to `Domain` (registrar) would either require + inventing a `domains.dns_manager_domain_id` bridge that doesn't exist + (out of scope, a Domain-module change, not a Hosting-module one) or + silently assume every hosting customer's domain is registrar-managed by + us, which isn't guaranteed. +- Keeping `dns_manager_domain_id` nullable also covers the CLI/admin flow + of provisioning hosting before any DNS zone exists for it — Plesk's + subscription can be created against a domain name string even if no + local `dns_manager_domain` row is made yet in Phase 1. + +## 7. Repository + +``` +app/Repositories/Module/HostingAccount/ + HostingAccountRepositoryInterface.php + HostingAccountRepository.php + HostingAccountCachedRepository.php +``` + +```php +interface HostingAccountRepositoryInterface extends BaseRepositoryInterface +{ + public function filter( + array $data = [], + array $relationships = [], + array $withCount = [], + array $select = ['*'], + bool $returnQuery = false + ); +} +``` + +`HostingAccountRepository::getModel()` → `ModuleHostingAccount::class`, +`filter()` implemented the same way as `HostingRepository::filter()` +(status/relations/pagination handling already shown in that file) — copy +that shape, don't invent a new query-building convention. + +`HostingAccountCachedRepository` wraps it exactly like +`DnsServerCachedRepository` does (`Cache::remember` on `filter`, pass- +through when `returnQuery`/`paginate` is set). + +Also add, on `App\Repositories\DnsManager\Server\DnsServerRepositoryInterface` +(the interface `ServerService` already depends on): + +```php +public function getUsableFor(ServerType $type): Collection; +// WHERE type = 'both' OR type = :type +``` + +Bind both interfaces explicitly in `RepositoryProvider`: + +```php +$this->app->singleton(HostingAccountRepositoryInterface::class, HostingAccountCachedRepository::class); +``` + +(And, while touching this file: confirm/fix the `DnsManager\Server` +binding gap noted in §1.1 — the new Hosting service depends on the same +interface `ServerService` uses, so if that resolution is currently +implicit it needs to keep working for both consumers, not just happen to +work for the one that existed first.) + +## 8. Service — Plesk integration (Phase 1 only) + +``` +app/Services/Plesk/ + PleskService.php // shared low-level HTTP: resolves a DnsServer → Http::withBasicAuth(...), used by all the others + PleskClientService.php // find-or-create Plesk customer + PleskDomainService.php // create/update/remove Plesk domain object — EXTRACTED from ServerService, not rewritten + PleskHostingService.php // orchestrates: customer → subscription → plan → persist +``` + +`PleskService` is the one new piece of shared plumbing, and it's a direct +copy of what `ServerService` already does per-call (`Http::withoutVerifying() +->withBasicAuth($server->username, Crypt::decryptString($server->password)) +->timeout(...)`), pulled out so `ServerService` (DNS) and the new +`PleskHostingService` both call the same code instead of two Basic-Auth +blocks drifting apart: + +```php +class PleskService +{ + public function client(DnsServer $server): PendingRequest + { + return Http::withoutVerifying() + ->withBasicAuth($server->username, Crypt::decryptString($server->password)) + ->timeout(30); + } +} +``` + +`PleskDomainService::createDomain()` is `ServerService::createDomain()` +moved verbatim (same `hosting_settings`/`plan` payload shape, same +`['error'=>bool,'data'=>...]` return) — `ServerService` then delegates to +it instead of duplicating the HTTP call, so DNS-module behavior doesn't +change. + +`PleskClientService` — new for Phase 1, Plesk has no "create domain +without a client" path that fits multi-tenant billing, so this is +required, not optional: + +```php +class PleskClientService +{ + public function __construct(private PleskService $plesk) {} + + public function findOrCreateCustomer(DnsServer $server, Customer $customer): array + { + // GET /clients?login=... ; if 404, POST /clients + // returns ['error' => bool, 'data' => ['id' => ..., 'guid' => ...]] + } +} +``` + +`PleskHostingService` — the Phase 1 entry point: + +```php +class PleskHostingService +{ + public function __construct( + private PleskClientService $clients, + private PleskDomainService $domains, + private HostingAccountRepositoryInterface $accounts, + ) {} + + public function provision(array $data): array + { + // $data: customer_id, dns_manager_server_id, dns_manager_domain_id?, + // domain_name, hosting_plan + + $server = ...; // DnsServer::findOrFail, must servesHosting() + + $client = $this->clients->findOrCreateCustomer($server, $customer); + if ($client['error']) return $client; + + $subscription = $this->domains->createDomain($server, [ + 'domain_name' => $data['domain_name'], + 'plesk_client_id' => $client['data']['id'], + 'plan' => $data['hosting_plan'], + ]); + if ($subscription['error']) return $subscription; + + $account = $this->accounts->create([ + 'customer_id' => $data['customer_id'], + 'service_id' => $data['service_id'] ?? null, + 'dns_manager_server_id' => $server->id, + 'dns_manager_domain_id' => $data['dns_manager_domain_id'] ?? null, + 'plesk_client_id' => $client['data']['id'], + 'plesk_subscription_id' => $subscription['data']['id'], + 'plesk_guid' => $subscription['data']['guid'], + 'hosting_plan' => $data['hosting_plan'], + 'ftp_username' => $subscription['data']['hosting_settings']['ftp_login'] ?? null, + 'system_username' => $subscription['data']['hosting_settings']['ftp_login'] ?? null, + 'document_root' => $subscription['data']['www_root'] ?? null, + 'status' => 'active', + ]); + + return ['error' => false, 'data' => $account]; + } +} +``` + +No `suspend()`/`unsuspend()`/`resetFtpPassword()` bodies are written yet — +the API routes for them exist (§9, per explicit request) but their +controller actions return `501`/"not implemented in this phase" until +those methods are built. Writing stub method bodies now that call nothing +would be scaffolding for a phase that hasn't been scoped — the route +existing is enough to reserve the contract. + +## 9. Controller & API + +``` +app/Http/Requests/ModuleHosting/CreateHostingAccountRequest.php +app/Http/Requests/ModuleHosting/UpdateHostingAccountRequest.php +app/Http/Resources/Api/Admin/HostingAccountResource.php +app/Http/Controllers/Api/Admin/Hosting/HostingAccountController.php +``` + +```php +// routes/api.php, alongside the existing DNS Manager group +Route::prefix('hosting-accounts') + ->controller(HostingAccountController::class) + ->group(function () { + Route::get('/', 'index')->middleware('perm:hosting.view'); + Route::get('{id}', 'show')->middleware('perm:hosting.view'); + Route::post('/', 'store')->middleware('perm:hosting.create'); + Route::put('{id}', 'update')->middleware('perm:hosting.edit'); + Route::delete('{id}', 'destroy')->middleware('perm:hosting.destroy'); + + Route::post('{id}/suspend', 'suspend')->middleware('perm:hosting.edit'); + Route::post('{id}/unsuspend', 'unsuspend')->middleware('perm:hosting.edit'); + Route::post('{id}/reset-ftp-password', 'resetFtpPassword')->middleware('perm:hosting.edit'); + }); +``` + +`store()` calls `PleskHostingService::provision()` and returns the +`HostingAccountResource`. `suspend`/`unsuspend`/`reset-ftp-password` +controller methods exist per the request's API list but return +`response()->json(['message' => 'Not implemented in this phase'], 501)` — +routed and permission-gated now, implemented when their phase lands. + +Add to `RolesAndPermissions.php` seeder: +```php +'hosting.view', 'hosting.create', 'hosting.edit', 'hosting.destroy', +``` + +## 10. Reusable existing code + +- `App\Services\ServerService::createDomain/updateDomain/removeDomain` → + extracted into `PleskDomainService`, reused by both modules (§8). +- Basic-Auth-over-`Http`-facade pattern → copied into `PleskService` + exactly as done in `ServerService`/`vCenter\Client`, no new HTTP layer. +- `Crypt::decryptString($server->password)` → same decryption call reused + as-is; no new encryption scheme introduced (explicitly not `SignRSA`, + which is the legacy module's scheme). +- `BaseRepository`/`BaseCachedRepository`/`BaseRepositoryInterface` → used + unchanged. +- `App\Traits\LogsActivity` → reused on `ModuleHostingAccount`, same as + `DnsServer`/`DnsDomain`/`ModuleHosting`. +- Migration style, permission-seeding call site, route-group style — all + copied from the DNS Manager section of `routes/api.php` and + `RolesAndPermissions.php`, not reinvented. +- `HostingRepository::filter()` (legacy, `app/Repositories/Module/Hosting/HostingRepository.php`) + → its filter/relation/status query shape is copied as the template for + the new repository's `filter()`, even though the legacy table itself is + not reused. + +## 11. Trade-offs + +| Decision | Trade-off accepted | +|---|---| +| `type` as `varchar` + `both` value | One extra `IN`-expansion needed at every "find a server for X" query site, centralized in `getUsableFor()`. In exchange: zero new tables, matches the table's existing `status` column style, and a future 4th type is a one-line enum case instead of an `ALTER TABLE ... MODIFY ENUM`. | +| No pivot/capability table | Can't express fine-grained per-capability metadata (e.g. "default server for hosting" flag) — not needed by anything requested; if it's needed later it's an additive migration, not a rewrite of this one. | +| `service_id` nullable, not required | Lets Phase 1 provision hosting without a full order/service pipeline wired up yet; costs a nullable check everywhere the relation is read. | +| `dns_manager_domain_id` nullable, no FK to `App\Models\Domain` | Keeps Hosting decoupled from Domain-module assumptions that don't hold today (registrar domain ≠ DNS-managed zone); costs one more nullable relation to explain in onboarding docs. | +| No password/Basic-Auth column on `module_hosting_accounts` | Every Plesk call for this account has to resolve back through `dns_manager_server_id` first — no local shortcut. In exchange: one credential source of truth, matches the explicit "don't store it twice" instruction, and avoids repeating `module_vmware.plain_password`. | +| Extracting `PleskDomainService` out of `ServerService` rather than writing a second Plesk client | Touches an existing, working file (small risk) instead of a net-new, isolated one (zero risk to DNS module). In exchange: avoids the exact duplication (`module_hosting_server` vs `dns_manager_servers`, two unrelated Plesk-ish clients) that already happened once in this codebase. | +| New `hosting.*` permission group instead of reusing seeded `module-hosting.*` | One more permission group to seed. In exchange: doesn't couple a new, modern API to whatever the legacy admin-blade screens currently do with `module-hosting.*`, and keeps the "flat name = modern services-era module" convention legible. | +| No stub bodies for suspend/unsuspend/reset-ftp-password | Routes exist per the request's API list but 501 until built — avoids writing speculative Plesk-call code for a phase not yet scoped, per explicit instruction to design Phase 1 only. | + +## 12. Explicitly not designed here + +Per instruction: Database, SSL, Mailbox, dedicated FTP account creation +beyond Plesk's default system user, Git deploy, source auto-upload, +Let's Encrypt. No schema columns, service methods, or routes for these are +included above, including no nullable "reserved for later" columns on +`module_hosting_accounts` — adding those now would be exactly the +speculative scaffolding this phase is meant to avoid. diff --git a/docs/open-api-plesk.md b/docs/open-api-plesk.md new file mode 100644 index 0000000..8339164 --- /dev/null +++ b/docs/open-api-plesk.md @@ -0,0 +1,4708 @@ +openapi: 3.0.3 +info: + version: v2 + title: Plesk RESTful API + description: >- + New remote API for Plesk is based on REST concepts and provides the output + in JSON format. +servers: + - url: /api/v2 + description: Plesk Server REST API +security: + - BasicAuth: [] + - APIKeyHeader: [] + - BasicAuth: [] + ImpersonateLoginHeader: [] + - APIKeyHeader: [] + ImpersonateLoginHeader: [] +tags: + - name: Authentication + description: Auth entry points + - name: Cli + description: CLI gate entry points + - name: Server + description: Server entry points + - name: Extensions + description: Extensions entry points + - name: Clients + description: Clients entry points + - name: Domains + description: Domains entry points + - name: FtpUsers + description: FTP Users entry points + - name: Databases + description: Databases, database servers and database users entry points + - name: DNS + description: DNS entry points + - name: Files + description: Domain file management entry points +paths: + /auth/keys: + post: + tags: + - Authentication + x-roles: + - admin + summary: Generate API secret key + description: >- + Create a new API secret key for authentication with specified login + credentials and IP restrictions. + responses: + '201': + description: Secret key was successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SecretKeyResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Incorrect login + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SecretKeyRequest' + /auth/keys/{key}: + delete: + tags: + - Authentication + x-roles: + - admin + summary: Delete API secret key + description: >- + Revoke and delete an API secret key, removing all associated access + permissions. + parameters: + - in: path + name: key + description: Key ID + required: true + example: 3d2e93ad-85e6-1a3e-9f81-b9d26650e4f9 + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Key does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /server/session: + post: + tags: + - Authentication + - Server + x-roles: + - admin + - reseller + - customer + summary: Create a session token + description: > + Create a new session for the specified user login. Returns a session + identifier + + that can be used to access Plesk panel via the URL: + + https://:/enterprise/rsession_init.php?PLESKSESSID= + responses: + '201': + description: Session was successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SessionResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SessionRequest' + /cli/commands: + get: + tags: + - Cli + x-roles: + - admin + summary: List CLI commands + description: >- + Retrieve a list of all available Plesk CLI commands that can be executed + via the REST API. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: string + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: true + /cli/{id}/ref: + get: + tags: + - Cli + x-roles: + - admin + x-plesk-since: '17.9' + summary: Get CLI command reference + description: >- + Retrieve detailed reference information for a specific Plesk CLI + command, including allowed subcommands and options. + parameters: + - in: path + name: id + description: Command name + required: true + example: settings + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + allowed_commands: + type: object + allowed_options: + type: object + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Command is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /cli/{id}/call: + post: + tags: + - Cli + x-roles: + - admin + summary: Execute CLI command + description: >- + Execute a Plesk CLI command (a Plesk utility, e.g. one run via `plesk + bin ` or `/usr/local/psa/bin/`) with the provided + parameters and environment. Set `fail_on_error` to true so that a failed + command is reported as an error (HTTP 422) instead of a 200 response + with a non-zero exit code. + parameters: + - in: path + name: id + description: Command name + required: true + example: settings + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CliCallResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Command is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + description: >- + Respond with 422 status when the command returns a non-zero exit + code. + content: + application/json: + schema: + $ref: '#/components/schemas/CliCallResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CliCallRequest' + /server: + get: + tags: + - Server + x-roles: + - admin + summary: Get server information + description: >- + Retrieve server metadata including Plesk version, platform, and system + information. + responses: + '200': + description: Return Server Meta Information + content: + application/json: + schema: + $ref: '#/components/schemas/ServerMeta' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: true + /server/init: + post: + tags: + - Server + x-roles: + - admin + summary: Initialize server + description: >- + Perform initial server configuration, including admin credentials and + basic settings. + responses: + '200': + description: Inital server setup was succesfully performed + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ServerInit' + /server/license: + post: + tags: + - Server + x-roles: + - admin + summary: Install license key + description: Install or update the Plesk or additional license key for the server. + responses: + '200': + description: License key was succesfully installed + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ServerLicense' + /server/ips: + get: + tags: + - Server + x-roles: + - admin + summary: List server IP addresses + description: Retrieve a list of all IP addresses configured on the server. + responses: + '200': + description: Return Server IP Addresses list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ServerIp' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: true + /extensions: + get: + tags: + - Extensions + x-roles: + - admin + summary: List installed extensions + description: >- + Retrieve a list of all Plesk extensions currently installed on the + server. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Extension' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: true + post: + tags: + - Extensions + x-roles: + - admin + summary: Install extension + description: >- + Install a Plesk extension from the Extension Catalog by ID, a file, or + URL. + responses: + '200': + description: Extension successfully installed + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExtensionInstallRequest' + /extensions/{id}: + get: + tags: + - Extensions + x-roles: + - admin + summary: Get extension details + description: Retrieve detailed information about a specific installed extension. + parameters: + - in: path + name: id + description: Extension identifier + required: true + example: letsencrypt + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Extension' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Extension is not installed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + delete: + tags: + - Extensions + x-roles: + - admin + summary: Uninstall extension + description: Remove an extension from the server and delete all associated data. + parameters: + - in: path + name: id + description: Extension identifier + required: true + example: letsencrypt + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Extension is not installed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /extensions/{id}/enable: + put: + tags: + - Extensions + x-roles: + - admin + summary: Enable extension + description: Enable a previously disabled extension, making it active on the server. + parameters: + - in: path + name: id + description: Extension identifier + required: true + example: letsencrypt + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Extension is not installed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /extensions/{id}/disable: + put: + tags: + - Extensions + x-roles: + - admin + summary: Disable extension + description: >- + Disable an extension temporarily without uninstalling it from the + server. + parameters: + - in: path + name: id + description: Extension identifier + required: true + example: letsencrypt + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Extension is not installed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /clients: + get: + tags: + - Clients + x-roles: + - admin + - reseller + - customer + summary: List clients + description: >- + Retrieve a list of all client accounts on the server that are available + to the user. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Client' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: true + post: + tags: + - Clients + x-roles: + - admin + - reseller + summary: Create client account + description: Create a new client account with the specified settings and permissions. + responses: + '201': + description: Client account successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ClientRequest' + /clients/{id}: + get: + tags: + - Clients + x-roles: + - admin + - reseller + - customer + summary: Get client details + description: Retrieve detailed information about a specific client account. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + put: + tags: + - Clients + x-roles: + - admin + - reseller + - customer + summary: Update client account + description: Update settings and properties of an existing client account. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ClientUpdateRequest' + delete: + tags: + - Clients + x-roles: + - admin + - reseller + summary: Delete client account + description: Delete a client account and all associated data from the server. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /clients/{id}/domains: + get: + tags: + - Clients + x-roles: + - admin + - reseller + - customer + summary: List client domains + description: Retrieve all domains associated with a specific client account. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DomainResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /clients/{id}/statistics: + get: + tags: + - Clients + x-roles: + - admin + - reseller + - customer + summary: Get client statistics + description: Retrieve resource usage statistics for a specific client account. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ClientStatistics' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /clients/{id}/activate: + put: + tags: + - Clients + x-roles: + - admin + - reseller + summary: Activate client + description: Activate a suspended client account, restoring access to all services. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /clients/{id}/suspend: + put: + tags: + - Clients + x-roles: + - admin + - reseller + summary: Suspend client + description: Suspend a client account, temporarily blocking access to all services. + parameters: + - in: path + name: id + required: true + description: Client ID + example: 7 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Client is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /domains: + get: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: List domains + description: >- + Retrieve a list of domains available to the user, with optional + filtering by domain name. + parameters: + - in: query + name: name + description: Filter data by domain name + example: example.com + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DomainResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: true + post: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Create domain + description: >- + Create a new domain with the specified configuration and hosting + settings. To create an addon domain, provide `base_domain`. To create a + subdomain, provide `parent_domain`. To assign the domain to a specific + client, provide `owner_client`; if omitted, the domain will be assigned + to the currently logged-in user. + responses: + '201': + description: Domain successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DomainRequest' + /domains/{id}: + get: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Get domain details + description: Retrieve detailed information about a specific domain. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DomainResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + put: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Update domain + description: Update settings and configuration for an existing domain. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DomainUpdateRequest' + delete: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Delete domain + description: Delete a domain and all associated data from the server. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreatedResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /domains/{id}/client: + get: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Get domain owner + description: Retrieve information about the client account that owns the domain. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /domains/{id}/status: + get: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Get domain status + description: >- + Retrieve the current operational status of a domain (active, suspended, + disabled). + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DomainStatus' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + put: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Update domain status + description: Change the operational status of a domain (enable, suspend, or disable). + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DomainStatus' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DomainStatus' + /domains/{id}/exec: + post: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Execute a shell command on a domain + description: >- + Execute a command in the context of a domain, running as the + subscription's system user with the domain's document root as the + default working directory, and retrieve its stdout, stderr and exit + code. Works even if the system user has no or limited shell access, and + allows operations on the domain content without root privileges. Set + `fail_on_error` to true so that a failed command is reported as an error + (HTTP 422) instead of a 200 response with a non-zero exit code. For + pipes, redirections or other shell features, wrap the command, e.g. bash + -lc ''. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DomainExecResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + description: >- + The command exited with a non-zero code (returned only when + fail_on_error is enabled). The command output is still included in + the body. + content: + application/json: + schema: + $ref: '#/components/schemas/DomainExecResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DomainExecRequest' + /domains/{id}/dns/resolution: + get: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Run a DNS resolution check for a domain + description: > + Runs a DNS troubleshooting report for the domain, returning a list of + checks + + (DNS management, web, mail, email authentication, propagation) with + severity, + + findings, and recommended actions, plus an aggregate summary. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DomainDnsResolutionResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Failed to execute the backend DNS check utility + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '501': + description: DNS resolution check is available in Plesk version >= 18.0.80 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /domains/{id}/logs: + get: + tags: + - Domains + x-roles: + - admin + - reseller + - customer + summary: Search domain logs + description: > + Search domain logs (Apache error logs, Nginx error logs, PHP-FPM error + logs, HTTP access logs, FTP xferlog, ModSecurity audit logs) for a + pattern and return matching entries with log statistics grouped by log + type. + + Only available on Linux-based Plesk servers. + parameters: + - in: path + name: id + required: true + description: Domain ID + example: 1 + schema: + type: integer + - in: query + name: pattern + required: false + description: > + Search pattern (regex) to filter log entries. + + + **Security restrictions**: Patterns with nested or adjacent + quantifiers are rejected to prevent ReDoS attacks. + + Examples of invalid patterns: `(a+)+`, `a*+`, `(error)+*` + + + Keep patterns simple and specific. Use character classes instead of + alternation where possible. + example: error|warning + schema: + type: string + - in: query + name: start_time + required: false + description: >- + Start time for log search in RFC3339 format (e.g., + 2026-05-21T14:30:00Z) + example: '2026-05-21T00:00:00Z' + schema: + type: string + format: date-time + - in: query + name: end_time + required: false + description: >- + End time for log search in RFC3339 format (e.g., + 2026-05-21T14:30:00Z). Must be >= start_time + example: '2026-05-22T00:00:00Z' + schema: + type: string + format: date-time + - in: query + name: sample_size + required: false + default: 5 + minimum: 1 + maximum: 500 + description: Maximum number of sample log entries to return + example: 10 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DomainLogsResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '501': + description: Not implemented (Windows servers) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /domains/{id}/fs: + get: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Get file or directory metadata + description: >- + Return stat metadata for a single file or directory. The default path is + the domain's document root. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: false + description: > + Target path. An absolute path (starting with "/" on Unix, or a drive + letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + + Defaults to the document root. + example: ./ + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found, or path does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + delete: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Delete a file or directory + description: >- + Delete a file or directory at the given path. Directories are deleted + recursively. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: true + description: > + Path to delete. An absolute path (starting with "/" on Unix, or a + drive letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + example: index.html + schema: + type: string + responses: + '204': + description: Deleted successfully. + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found, or path does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /domains/{id}/fs/list: + get: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: List directory contents + description: >- + List files and directories at the specified path. By default the listing + is rooted at the domain's document root. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: false + description: > + Directory to list. An absolute path (starting with "/" on Unix, or a + drive letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + + Defaults to the document root. + example: ./ + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found or path is not a directory. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /domains/{id}/fs/content: + get: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Download file contents + description: > + Download the contents of a regular file. Without `encoding`, the + response is a raw octet-stream + + attachment. When `encoding` is specified, the response is a JSON body + with the file contents + + encoded accordingly. Files larger than 100 MB cannot be downloaded via + the JSON mode. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: true + description: > + Target file path. An absolute path (starting with "/" on Unix, or a + drive letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + example: index.html + schema: + type: string + - in: query + name: encoding + required: false + description: >- + When specified, returns a `FileContentResponse` JSON body instead of + an octet-stream. + schema: + type: string + enum: + - plain + - base64 + responses: + '200': + description: >- + OK — raw octet-stream (no `encoding`) or `FileContentResponse` JSON + (with `encoding`). + content: + application/json: + schema: + $ref: '#/components/schemas/FileContentResponse' + application/octet-stream: + schema: + type: string + format: binary + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found, or path does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + put: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Upload file contents + description: > + Upload the request body as a file at the given path. Creates the file if + it does not exist; overwrites it if it does. + + + When sending `Content-Type: application/octet-stream`, the raw request + body is written verbatim. + + When sending `Content-Type: application/json`, the body must be a + `FileContentRequest` object. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: true + description: > + Destination file path. An absolute path (starting with "/" on Unix, + or a drive letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + example: index.html + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileContentRequest' + application/octet-stream: + schema: + type: string + format: binary + patch: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Append a chunk to a file + description: > + Append the request body to the end of a file at the given path. Creates + the file if it does not exist. + + + When sending `Content-Type: application/octet-stream`, the raw request + body is appended verbatim. + + When sending `Content-Type: application/json`, the body must be a + `FileContentRequest` object. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: true + description: > + Destination file path. An absolute path (starting with "/" on Unix, + or a drive letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + example: upload.bin + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileContentRequest' + application/octet-stream: + schema: + type: string + format: binary + /domains/{id}/fs/mkdir: + post: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Create a directory + description: Create a new directory within the domain's webspace. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: true + description: > + Path of the directory to create. An absolute path (starting with "/" + on Unix, or a drive letter such as "C:\\" on Windows) is used as-is; + + otherwise the path is resolved relative to the domain's document + root. + example: uploads + schema: + type: string + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Path already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /domains/{id}/fs/copy: + post: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Copy a file or directory + description: >- + Copy a file or directory from source to destination within the domain's + webspace. Returns stat metadata of the resulting entry at the + destination path. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or a path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found, or source path does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Destination already exists and overwrite is false. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileCopyMoveRequest' + /domains/{id}/fs/move: + post: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Move or rename a file or directory + description: >- + Move (rename) a file or directory from source to destination within the + domain's webspace. Returns stat metadata of the resulting entry at the + destination path. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or a path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found, or source path does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Destination already exists and overwrite is false. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileCopyMoveRequest' + /domains/{id}/fs/chmod: + put: + tags: + - Files + x-roles: + - admin + - reseller + - customer + summary: Change file or directory permissions + description: >- + Set the permission mode (chmod) of a file or directory within the + domain's webspace. Has no effect on Windows. Returns the updated stat + metadata. + parameters: + - in: path + name: id + required: true + description: Domain ID. + example: 1 + schema: + type: integer + - in: query + name: path + required: true + description: >- + Path to the file or directory. An absolute path (starting with "/" + on Unix, or a drive letter such as "C:\" on Windows) is used as-is; + otherwise relative to the document root. + example: index.html + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/File' + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: >- + Access to the specified domain is denied, or the path is outside the + webspace. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Domain not found, or the path does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileChmodRequest' + /ftpusers: + get: + tags: + - FtpUsers + x-roles: + - admin + - reseller + - customer + summary: List FTP users + description: >- + Retrieve a list of FTP users with optional filtering by username or + domain. + parameters: + - in: query + name: name + description: Filter data by user name + example: exampleuser + schema: + type: string + - in: query + name: domain + description: Filter data by domain name + example: example.com + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FtpUser' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain/user is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + post: + tags: + - FtpUsers + x-roles: + - admin + - reseller + - customer + summary: Create FTP user + description: >- + Create a new FTP user account with specified permissions and home + directory. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/FtpUser' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FtpUserRequest' + /ftpusers/{name}: + put: + tags: + - FtpUsers + x-roles: + - admin + - reseller + - customer + summary: Update FTP user + description: >- + Update FTP user settings such as password, home directory, or + permissions. + parameters: + - in: path + name: name + description: FTP user name + required: true + example: exampleuser + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: User is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FtpUserUpdateRequest' + delete: + tags: + - FtpUsers + x-roles: + - admin + - reseller + - customer + summary: Delete FTP user + description: Delete an FTP user account and revoke all associated access. + parameters: + - in: path + name: name + description: FTP User name + required: true + example: exampleuser + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: User is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /databases: + get: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: List databases + description: >- + Retrieve a list of all databases on the server, with optional filtering + by domain name. + parameters: + - in: query + name: domain + description: Filter data by domain name + example: example.com + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Database' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Server is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + post: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: Create database + description: Create a new database on the specified database server. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Database' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseRequest' + /databases/{id}: + delete: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: Delete database + description: Delete a database and all its data from the database server. + parameters: + - in: path + name: id + description: Database ID + required: true + example: 10 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Database is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /dbusers: + get: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: List database users + description: >- + Retrieve a list of database users, with optional filtering by database + ID. + parameters: + - in: query + name: dbId + description: Filter data by database ID + example: 44 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DatabaseUser' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain/database is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + post: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: Create database user + description: >- + Create a new database user with specified credentials and database + association. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseUser' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Server/database/domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseUserRequest' + /dbusers/{id}: + put: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: Update database user + description: Update database user settings, such as password or access permissions. + parameters: + - in: path + name: id + description: Database user ID + required: true + example: 12 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Server/database/domain is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseUserUpdateRequest' + delete: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: Delete database user + description: Delete a database user and revoke all associated access rights. + parameters: + - in: path + name: id + description: Database user ID + required: true + example: 12 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Database user is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + /dbservers: + get: + tags: + - Databases + x-roles: + - admin + - reseller + - customer + summary: List database servers + description: Retrieve a list of all database servers configured on the system. + parameters: + - in: query + name: id + description: Filter data by database server id + example: 1 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DatabaseServer' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Server is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + /dns/records: + get: + tags: + - DNS + x-roles: + - admin + - reseller + - customer + summary: List DNS records + description: Retrieve all DNS records for a specific domain or domain alias. + parameters: + - in: query + name: domain + description: Filter by domain or domain alias name + required: true + example: example.com + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DnsRecord' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Can not find DNS record by id '123' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + post: + tags: + - DNS + x-roles: + - admin + - reseller + - customer + summary: Create DNS record + description: Add a new DNS record to a domain or domain alias zone. + parameters: + - in: query + name: domain + description: Filter by domain or domain alias name + required: true + example: example.com + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DnsRecord' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: Domain or Domain Alias is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DnsRecordRequest' + /dns/records/{id}: + get: + tags: + - DNS + x-roles: + - admin + - reseller + - customer + summary: Get DNS record + description: Retrieve details of a specific DNS record by its ID. + parameters: + - in: path + name: id + description: DNS record ID + required: true + example: 123 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DnsRecord' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: DNS record is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: true + put: + tags: + - DNS + x-roles: + - admin + - reseller + - customer + summary: Update DNS record + description: Modify an existing DNS record's properties and values. + parameters: + - in: path + name: id + description: DNS record ID + required: true + example: 123 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: DNS record is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DnsRecordRequest' + delete: + tags: + - DNS + x-roles: + - admin + - reseller + - customer + summary: Delete DNS record + description: Remove a DNS record from the domain's DNS zone. + parameters: + - in: path + name: id + description: DNS record ID + required: true + example: 123 + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '403': + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionDeniedResponse' + '404': + description: DNS record is not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-readonly: false +components: + securitySchemes: + BasicAuth: + type: http + scheme: basic + description: HTTP Basic Authentication + APIKeyHeader: + type: apiKey + in: header + name: X-API-Key + description: API Key Authentication + ImpersonateLoginHeader: + type: apiKey + in: header + name: X-Impersonate-Login + description: >- + Allows admin users to impersonate another user account. The API request + will be executed in the context of the specified user. Only + administrators can use this header. + schemas: + StatusResponse: + type: object + properties: + status: + description: Result of the operation + type: string + example: success + required: + - status + CreatedResponse: + type: object + properties: + id: + description: Entity with the specified ID successfully created + type: integer + example: 100 + guid: + description: Entity successfully created, new GUID is + type: string + example: 18b78dd1-2b43-44f7-9599-56ccb56a85dc + required: + - id + - guid + ErrorResponse: + type: object + properties: + code: + type: integer + example: 400 + message: + type: string + example: Account with this name already exists + required: + - code + - message + PermissionDeniedResponse: + type: object + properties: + code: + type: integer + example: 0 + message: + type: string + example: Permission denied + required: + - code + - message + SecretKeyRequest: + type: object + description: Secret key parameters + properties: + ip: + description: >- + The IP address that will be linked to the key. If this node or 'ips' + node is not specified, the IP address of the request sender will be + used. + type: string + example: 195.214.233.128 + ips: + description: >- + Array of IP addresses that will be linked to the key. If this node + or 'ip' node is not specified, the IP address of the request sender + will be used. + type: array + items: + type: string + example: + - 93.184.216.34 + - 2606:2800:220:1:248:1893:25c8:1946 + login: + description: >- + The login name of an existing customer or a reseller that will have + this secret key. The customer's or reseller's account should be + active. If this node is not specified, the administrator's login + will be used. + type: string + example: admin + description: + description: Additional information about the key. + type: string + example: Secret key for Administrator + SecretKeyResponse: + type: object + properties: + key: + description: Secret key value. + type: string + example: c5b239d1-8eb6-8cee-76b7-aa16be37fee8 + required: + - key + SessionRequest: + type: object + description: Session creation parameters + required: + - login + properties: + login: + description: The login name of the user to create a session for. + type: string + example: admin + user_ip: + description: > + The IP address of the user who will use the session. If not + specified, the IP address of the request sender will be used. + type: string + example: 192.168.1.100 + skip_mfa: + description: Skip multi-factor authentication check for the created session. + type: boolean + default: false + example: true + SessionResponse: + type: object + properties: + id: + description: >- + The session identifier. Use it to access the Plesk panel via: + https://:/enterprise/rsession_init.php?PLESKSESSID= + type: string + example: ab1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e + required: + - id + CliCallRequest: + type: object + description: Command execution parameters + properties: + params: + type: array + items: + type: string + example: + - --get + - FullHostName + env: + type: object + example: + PSA_PASSWORD: password + fail_on_error: + description: >- + Respond with 422 status when the command returns a non-zero exit + code. Recommended to enable when executing commands so failures are + reported as errors instead of a successful response. + type: boolean + default: false + example: true + CliCallResponse: + type: object + description: Command execution result + properties: + code: + description: Command return code. + type: integer + example: 0 + stdout: + description: Command stdout. + type: string + example: Done + stderr: + description: Command stderr. + type: string + example: Execution failed + ServerMeta: + type: object + description: Server Meta Information + properties: + platform: + description: Server platform type. + type: string + example: Unix + hostname: + description: Server hostname. + type: string + example: example.com + guid: + description: Server GUID. + type: string + example: baa58292-fee0-4ecb-9da1-bba0cc5adea7 + panel_version: + description: Product version. + type: string + example: 17.9.3 + panel_revision: + description: Product build revision. + type: string + example: 1711e2bceff6fd6656c821b8cb6444c2726f3784 + panel_build_date: + description: Product ISO build date. + type: string + format: date + example: '2018-06-20' + panel_update_version: + description: Product installed update version. + type: string + example: '10' + extension_version: + description: REST API extension version. + type: string + example: 1.1.0 + extension_release: + description: REST API release number. + type: string + example: '10' + required: + - platform + - hostname + - guid + - panel_version + - panel_revision + - panel_build_date + - panel_update_version + - extension_version + - extension_release + ServerInit: + type: object + description: Initial server setup parameters + properties: + admin: + $ref: '#/components/schemas/Admin' + password: + description: >- + The new Plesk Administrator's password that will replace the default + one. + type: string + example: setup + server_name: + description: The full host name. + type: string + example: my-plesk-server.com + required: + - admin + - password + Admin: + type: object + description: Plesk administrator information + properties: + name: + description: The administrator's personal name. + type: string + example: John Smith + email: + description: The administrator's e-mail address. + type: string + format: email + example: john_smith@plesk.com + company: + description: The administrator's company name. + type: string + example: Plesk + phone: + description: The administrator's phone number. + type: string + example: +41 31 528 12 23 + fax: + description: The administrator's fax number. + type: string + example: +41 31 528 12 23 + address: + description: The administrator's street address. + type: string + example: Vordergasse 59 + city: + description: The name of the city where administrator lives. + type: string + example: Schaffhausen + state: + description: >- + The name of state (for US citizens) or province where administrator + lives. + type: string + example: Kanton Schaffhausen + post_code: + description: The administrator's zip/postal code. + type: string + example: '8200' + country: + description: The name of the country where administrator lives. + type: string + example: CH + send_announce: + description: >- + It specifies whether Plesk announcement messages are sent to the + administrator's e-mail. + type: boolean + example: false + locale: + description: The locale used by the administrator. + type: string + example: en-US + multiple_sessions: + description: >- + It specifies the information if the administrator has multiple + sessions in Plesk. + type: boolean + example: false + required: + - name + - email + ServerIp: + type: object + description: Server IP Addresses + properties: + ipv4: + description: The IP address in Plesk database (IPv4). + type: string + format: ipv4 + example: 93.184.216.34 + ipv6: + description: The IP address in Plesk database (IPv6). + type: string + format: ipv6 + example: 2606:2800:220:1:248:1893:25c8:1946 + netmask: + description: The netmask of the network. + type: string + example: 255.255.255.0 + interface: + description: The server network interface name. + type: string + example: eth0 + type: + description: 'The type of IP address. Allowed values: shared | exclusive.' + type: string + enum: + - shared + - exclusive + example: exclusive + required: + - netmask + - interface + - type + ServerLicense: + type: object + description: License parameters + properties: + key: + description: License key data. + type: string + example: >- + /RjdfLic1RwpNPkEnLlxNJkJCPzNDWSRDOyVeJDI/MSo1RlFEMD4+VTEzKTk3S0pfVjFOJTZFXzMpTEVPQC5JRjdUR0RZCk0hMSJYOzYhVFxPI14jTFJPRSlaRFI0VjZWPEYiT1s5T0opJV8+NCMnUigvQ1o7K0U+QEtbISc1MlMqLV0KTSorUDVfMzxHNUkrSksxWzZVNVZJXTc5T18xQVNNNzRXWTo3WVkmW0I/J2AnYC4iJTlTP1grSCdYS0E+RApNPVteRk43RCNAVFpVO0dBO1hZMjo+K0pQNEsiLj5UUDQlLDA7XyVZIVo/QypGJiJcW1FbJF1fVVxUJy5ECk1HXCpSUzwtUVYoJzwsVV47MT5bJD8wWlgyYCo/KTM0QUJFMmAuOjc7QypMWylfQiFVP05PRj05LCY5SkgKTVRTWE9cRi84OUgxJDpgLl5RPEtPPkE4RCUtXU06NixHLi1ZL0ZBVSkyVkBVUCgiN0YrKyoyXTJMJ08qVwpNIk4lOyFeLFwmIiQ4NkBgMUEmTDRcWDdJKihZTEMhTENOJEMiKV0wIzkkSU4qMTEzO1BIJjo4K0U3LzU6Ck0wPCNTUytONjAhQEFROCFbUDI5OktVLlEsMlY9ND1gSjpgJVMjLS4tVzsrRzpAME8yVTpLVmAiT1NHLFAKTVU0SSowNVo1IShEWipAO10tRlsxQldVSkZSKDcyPjpWLik5I0YzWD0yVTs9NTlNL1dASyglXk5YN1U4UgpNTV8sXD0tRTQuLkBFMVk2I0REUCItXyVMNlRPRkEyTEA7Pl9QO10jUSI6I1tNIzVRU0VZVE1HWDInIzk4Ck1XRUNPVz5CSFUjUSo4K0IvOFlDWSNTLydXSls7KD9VTD8+WzUqQEA5Uic0S1BMJSowOk9fQ0QvQThUVCEKTT1AKiJFKztAKCFXNl9ORDhUVlleOCU5JS8rJzc+JEZUXigzMDs4PDtbNkUpPD8vUiJAREQoTkxUPlQ2JgpNVFdbVz9bT10mVy5FVEkyXD5MPSIkMkRVVipKWTolXVhRTzkoM1tLLSJRJFlXNTRTYEBNWy1UUV09K04wCk1SVWBbLyckXj5KXjsnUzgyRzE3QVUlWilXX0ZCX1BGMkRMSThHXjZBWE5FTUBEMjBgRTomVTpVLy1WRCQKTVtWUyQ+XDcsRUA+XU0hXz4pKFxBJ1JCNUQ6XCEnQjoyPFw + code: + description: Activation code. + type: string + example: AB1C23-4DEF56-7GHI89-JK1L23-MNP456 + additional: + description: It specifies if the key that will be installed is an additional key. + type: boolean + example: false + Extension: + type: object + description: Extension entity + properties: + id: + description: Extension identifier. + type: string + example: letsencrypt + name: + description: Human readable name. + type: string + example: Let's Encrypt + version: + description: Extension version. + type: string + example: 2.6.1 + release: + description: Extension release number. + type: string + example: '398' + active: + description: Extension status. + type: boolean + example: true + required: + - id + - name + - version + - release + - active + ExtensionInstallRequest: + type: object + description: Extension installation request + properties: + id: + description: Installation by Identifier. + type: string + example: letsencrypt + url: + description: Installation by URL. + type: string + example: https://example.com/catalog/letsencrypt.zip + file: + description: Installation by file. + type: string + example: /path/to/extension.zip + Client: + type: object + description: The client entity + properties: + id: + description: The client's ID. + type: integer + example: 7 + created: + description: Creation date. + type: string + format: date + example: '2016-11-13' + name: + description: Client's personal name (1 to 60 characters long). + type: string + example: John Smith + company: + description: The company name (0 to 60 characters long). + type: string + example: Plesk + login: + description: The login name of the client account (1 to 60 characters long). + type: string + x-validators: + - UniqueClientLogin + example: john-unit-test + status: + description: >- + The current status of the client account. Allowed values: 0 (active) + | 16 (disabled by admin) | 4 (under backup/restore) | 256 (expired). + type: integer + example: 0 + email: + description: >- + The email address of the client account owner (0 to 255 characters + long). + type: string + example: john_smith@msn.com + locale: + description: 'The locale used on the client account. Default value: en-US.' + type: string + example: en-US + guid: + description: The global user ID of the client account just added to Plesk. + type: string + example: 18b78dd1-2b43-44f7-9599-56ccb56a85dc + owner_login: + description: >- + The login name of a client account owner. If the client account + owner is Plesk Administrator, specify the admin login name. + type: string + example: admin + external_id: + description: >- + The client GUID in the Plesk components (for example, Business + Manager). + type: string + example: link:12345 + description: + description: The description for the client account. + type: string + example: Nice guy + type: + description: >- + The type of the client account. Allowed values: reseller | customer + | admin. + type: string + enum: + - reseller + - customer + - admin + example: reseller + required: + - id + - created + - name + - company + - login + - status + - email + - locale + - guid + - description + - type + ClientRequest: + type: object + description: The client entity + properties: + name: + description: Client's personal name (1 to 60 characters long). + type: string + example: John Smith + company: + description: The company name (0 to 60 characters long). + type: string + example: Plesk + login: + description: The login name of the client account (1 to 60 characters long). + type: string + x-validators: + - UniqueClientLogin + example: john-unit-test + status: + description: >- + The current status of the client account. Allowed values: 0 (active) + | 16 (disabled by admin) | 4 (under backup/restore) | 256 (expired). + type: integer + example: 0 + email: + description: >- + The email address of the client account owner (0 to 255 characters + long). + type: string + example: john_smith@msn.com + locale: + description: 'The locale used on the client account. Default value: en-US.' + type: string + example: en-US + owner_login: + description: >- + The login name of a client account owner. If the client account + owner is Plesk Administrator, specify the admin login name. + type: string + example: admin + external_id: + description: >- + The client GUID in the Plesk components (for example, Business + Manager). + type: string + example: link:12345 + description: + description: The description for the client account. + type: string + example: Nice guy + password: + description: The password of the client account (5 to 14 characters long). + type: string + x-validators: + - PasswordStrength + example: changeme1Q** + type: + description: >- + The type of the client account. Allowed values: reseller | customer + | admin. + type: string + enum: + - reseller + - customer + - admin + example: reseller + required: + - name + - login + - password + - email + - type + ClientUpdateRequest: + type: object + description: The client entity + properties: + name: + description: Client's personal name (1 to 60 characters long). + type: string + example: John Smith + company: + description: The company name (0 to 60 characters long). + type: string + example: Plesk + login: + description: The login name of the client account (1 to 60 characters long). + type: string + x-validators: + - UniqueClientLogin + example: john-unit-test + status: + description: >- + The current status of the client account. Allowed values: 0 (active) + | 16 (disabled by admin) | 4 (under backup/restore) | 256 (expired). + type: integer + example: 0 + email: + description: >- + The email address of the client account owner (0 to 255 characters + long). + type: string + example: john_smith@msn.com + locale: + description: 'The locale used on the client account. Default value: en-US.' + type: string + example: en-US + owner_login: + description: >- + The login name of a client account owner. If the client account + owner is Plesk Administrator, specify the admin login name. + type: string + example: admin + external_id: + description: >- + The client GUID in the Plesk components (for example, Business + Manager). + type: string + example: link:12345 + description: + description: The description for the client account. + type: string + example: Nice guy + password: + description: The password of the client account (5 to 14 characters long). + type: string + x-validators: + - PasswordStrength + example: changeme1Q** + ClientStatistics: + type: object + description: Client's usage + properties: + active_domains: + description: The number of active domains created by a client. + type: integer + example: 1 + subdomains: + description: The number of sub-domains created by a client. + type: integer + example: 0 + disk_space: + description: The amount of disk space occupied by the given a client. + type: integer + example: 765952 + email_postboxes: + description: The number of email boxes created by a client. + type: integer + example: 1 + email_redirects: + description: The number of redirects created by a client. + type: integer + example: 0 + email_response_messages: + description: The number of automatic response messages created by a client. + type: integer + example: 0 + mailing_lists: + description: The number of mailing lists created by a client. + type: integer + example: 0 + databases: + description: The number of databases used by a client. + type: integer + example: 0 + traffic: + description: The amount of traffic (in bytes) spent by a client monthly. + type: integer + example: 0 + traffic_prevday: + description: >- + The amount of traffic (in bytes) spent by a client during the + previous day. + type: integer + example: 0 + required: + - active_domains + - subdomains + - disk_space + - email_postboxes + - email_redirects + - email_response_messages + - mailing_lists + - databases + - traffic + - traffic_prevday + DomainRequest: + type: object + properties: + name: + description: Domain name. + type: string + example: example.com + description: + description: The description for the domain. + type: string + example: My website + hosting_type: + description: >- + It specifies the hosting type of the created domain. Allowed values: + `virtual` | `standard_forwarding` | `frame_forwarding` | `none`. + + Meanings: `virtual` - the domain ships with physical hosting. + + `standard_forwarding` - the domain ships with standard forwarding. + When the user goes to a domain on which the standard forwarding is + set, Plesk redirects this user from the requested URL to the + destination URL. This is done explicitly: the user sees the real + ‘destination’ address in the path bar of the browser. + + `frame_forwarding` - the domain ships with frame forwarding. When + the user goes to such site on which frame forwarding is set, Plesk + redirects this user from the requested URL to the ‘destination’ URL + implicitly (the user still sees the initial URL in the path bar of + the browser). + + `none` - no hosting ships with the domain. + type: string + enum: + - virtual + - standard_forwarding + - frame_forwarding + - none + hosting_settings: + description: >2- + Fields depend on the `hosting_type` value: + for `virtual`: + + `ftp_login` - FTP user login name (required when creating a + subscription). `ftp_password` - FTP user password (required when + creating a subscription). `www_root` - WWW root directory for the + domain (optional, defaults to the domain name). + + for `standard_forwarding`: + + `dest_url` - URL to redirect the visitor to (required). `http_code` + - HTTP redirect code; allowed values: `301`, `302` (optional, + defaults to `301`). + + for `frame_forwarding`: + + `dest_url` - URL to load inside the frame (required). + + for `none`: no fields are expected. + type: object + properties: + ftp_login: + type: string + x-validators: + - UniqueFtpLogin + example: login + ftp_password: + type: string + example: qwe123 + www_root: + type: string + description: WWW root directory for the domain (optional). + example: sub.example.com + dest_url: + type: string + example: https://example.com + http_code: + type: integer + enum: + - 301 + - 302 + example: 301 + additionalProperties: false + example: + ftp_login: test_login + ftp_password: changeme1Q** + base_domain: + $ref: '#/components/schemas/DomainReference' + parent_domain: + $ref: '#/components/schemas/DomainReference' + owner_client: + $ref: '#/components/schemas/OwnerClientReference' + ip_addresses: + description: The IP address associated with the domain. + type: array + items: + type: string + example: + - 93.184.216.34 + - 2606:2800:220:1:248:1893:25c8:1946 + ipv4: + description: '@deprecated' + type: array + items: + type: string + format: ipv4 + example: + - 93.184.216.34 + ipv6: + description: '@deprecated' + type: array + items: + type: string + format: ipv6 + example: + - 2606:2800:220:1:248:1893:25c8:1946 + plan: + $ref: '#/components/schemas/PlanReference' + DomainResponse: + type: object + properties: + id: + description: Domain ID. + type: integer + example: 7 + name: + description: Domain name. + type: string + example: example.com + ascii_name: + description: Domain display name. + type: string + example: ascii-example.com + hosting_type: + description: Domain hosting type. + type: string + enum: + - virtual + - standard_forwarding + - frame_forwarding + - none + base_domain_id: + description: Base domain ID. + type: integer + example: 3 + www_root: + description: WWW Root on filesystem + type: string + example: /var/www/vhosts/example.com/httpdocs + guid: + description: Domain GUID. + type: string + example: b623e93d-dc72-4102-b5f0-ded427cf0fb1 + created: + description: Creation date. + type: string + format: date + example: '2016-11-13' + aliases: + x-since: 18.0.42 + description: Domain aliases that point to this domain. + type: array + items: + $ref: '#/components/schemas/DomainAlias' + required: + - id + - name + - ascii_name + - hosting_type + - base_domain_id + - www_root + - guid + - created + DomainReference: + type: object + description: >- + Reference to a domain. At least one of id, name, or guid must be + provided. + anyOf: + - title: id + required: + - id + - title: name + required: + - name + - title: guid + required: + - guid + properties: + id: + description: Domain ID. + type: integer + example: 7 + name: + description: Domain name. + type: string + example: example.com + guid: + description: Domain GUID. + type: string + example: b623e93d-dc72-4102-b5f0-ded427cf0fb1 + DomainUpdateRequest: + type: object + properties: + name: + description: Domain name. + type: string + example: example.com + description: + description: The description for the domain. + type: string + example: My website + hosting_type: + description: >- + Hosting type. Allowed values: `virtual` | `standard_forwarding` | + `frame_forwarding` | `none`. + type: string + enum: + - virtual + - standard_forwarding + - frame_forwarding + - none + hosting_settings: + description: >2- + Fields depend on the `hosting_type` value: + for `virtual`: + + `ftp_login` - FTP user login name (required when creating a + subscription). `ftp_password` - FTP user password (required when + creating a subscription). `www_root` - WWW root directory for the + domain (optional, defaults to the domain name). + + for `standard_forwarding`: + + `dest_url` - URL to redirect the visitor to (required). `http_code` + - HTTP redirect code; allowed values: `301`, `302` (optional, + defaults to `301`). + + for `frame_forwarding`: + + `dest_url` - URL to load inside the frame (required). + + for `none`: no fields are expected. + type: object + properties: + ftp_login: + type: string + x-validators: + - UniqueFtpLogin + example: login + ftp_password: + type: string + example: qwe123 + www_root: + type: string + description: WWW root directory for the domain (optional). + example: sub.example.com + dest_url: + type: string + example: https://example.com + http_code: + type: integer + enum: + - 301 + - 302 + example: 301 + additionalProperties: false + example: + ftp_login: test_login + ftp_password: changeme1Q** + owner_client: + $ref: '#/components/schemas/OwnerClientReference' + ip_addresses: + description: The IP address associated with the domain. + type: array + items: + type: string + example: + - 93.184.216.34 + - 2606:2800:220:1:248:1893:25c8:1946 + ipv4: + description: '@deprecated' + type: array + items: + type: string + format: ipv4 + example: + - 93.184.216.34 + ipv6: + description: '@deprecated' + type: array + items: + type: string + format: ipv6 + example: + - 2606:2800:220:1:248:1893:25c8:1946 + plan: + $ref: '#/components/schemas/PlanReference' + DomainStatus: + type: object + properties: + status: + description: Status of the specified domain. + type: string + enum: + - active + - suspended + - disabled + example: active + required: + - status + DomainExecRequest: + type: object + description: Domain shell command execution parameters + properties: + command: + description: >- + Command to execute with its arguments. First element is the command + name, subsequent elements are arguments. + type: array + items: + type: string + minLength: 1 + example: + - whoami + stdin: + description: Standard input to pass to the command. + type: string + example: '' + env: + description: Environment variables to set for the command execution. + type: object + additionalProperties: + type: string + example: + APP_ENV: production + working_directory: + description: >- + Working directory for the command. Defaults to the domain's document + root. + type: string + example: public + fail_on_error: + description: >- + Respond with 422 status when the command returns a non-zero exit + code. Recommended to enable when executing commands so failures are + reported as errors instead of a successful response. + type: boolean + default: false + example: true + required: + - command + DomainExecResponse: + type: object + description: Domain shell command execution result + properties: + code: + description: Command return code (exit status). + type: integer + example: 0 + stdout: + description: Command standard output. + type: string + example: root + stderr: + description: Command standard error output. + type: string + example: '' + required: + - code + - stdout + - stderr + DomainLogEntry: + type: object + description: Single domain log entry + properties: + path: + description: Log file path + type: string + example: /var/www/vhosts/example.com/logs/error_log + line: + description: Raw log line content + type: string + example: >- + [Mon May 26 10:15:32 2026] [error] [client 192.168.1.1] File does + not exist: /var/www/vhosts/example.com/httpdocs/missing.html + required: + - path + - line + LogTypeInfo: + type: object + description: Log type information with count and time range + properties: + count: + description: Number of log entries found for this log type + type: integer + example: 42 + time_range: + description: Time range specific to this log type + type: object + required: + - min + - max + properties: + min: + description: Earliest timestamp for this log type, null if no logs found + type: string + format: date-time + nullable: true + example: '2026-05-21T10:00:00Z' + max: + description: Latest timestamp for this log type, null if no logs found + type: string + format: date-time + nullable: true + example: '2026-05-22T15:30:00Z' + required: + - count + - time_range + DomainLogsResponse: + type: object + description: Domain logs search response with per-log-type time ranges + properties: + entries: + description: Array of sample log entries matching the search criteria + type: array + items: + $ref: '#/components/schemas/DomainLogEntry' + log_type_counts: + description: Log entry counts grouped by log type with individual time ranges + type: object + additionalProperties: + $ref: '#/components/schemas/LogTypeInfo' + example: + apache_error: + count: 5 + time_range: + min: '2026-05-21T10:00:00Z' + max: '2026-05-22T15:30:00Z' + nginx_error: + count: 3 + time_range: + min: '2026-05-22T08:00:00Z' + max: '2026-05-22T14:00:00Z' + php_fpm_error: + count: 7 + time_range: + min: '2026-05-22T12:00:00Z' + max: '2026-05-22T15:00:00Z' + access: + count: 1523 + time_range: + min: '2026-05-21T00:00:00Z' + max: '2026-05-22T23:59:59Z' + xferlog: + count: 42 + time_range: + min: '2026-05-21T05:00:00Z' + max: '2026-05-22T20:00:00Z' + modsecurity_error: + count: 8 + time_range: + min: '2026-05-22T10:30:00Z' + max: '2026-05-22T14:45:00Z' + required: + - entries + - log_type_counts + DnsCheck: + type: object + description: A single DNS check result + properties: + category: + type: string + enum: + - dns-management + - web + - mail + - email-auth + - propagation + example: mail + name: + type: string + example: A record + result: + type: string + enum: + - Pass + - Fail + - Skipped + - Could not determine + example: Fail + fact: + type: string + example: 'A record: 203.0.113.5. This server uses 198.51.100.10.' + context: + type: string + example: 203.0.113.5 serves IPv4 visitors, not this server. + evidence: + description: >- + Free-form, check-specific supporting data (records, TTL, resolver, + timestamp). + type: object + additionalProperties: true + required: + - category + - name + - result + - fact + - context + DomainDnsResolutionResponse: + type: object + description: Domain DNS resolution check report + properties: + summary: + description: Neutral counts per result; no overall verdict. + type: object + additionalProperties: true + example: + total: 15 + counts: + Pass: 9 + Fail: 3 + Skipped: 2 + Could not determine: 1 + checks: + type: array + items: + $ref: '#/components/schemas/DnsCheck' + required: + - summary + - checks + OwnerClientReference: + type: object + description: Reference to a client. One of the following properties is required. + anyOf: + - title: id + required: + - id + - title: login + required: + - login + - title: guid + required: + - guid + - title: external_id + required: + - external_id + properties: + id: + description: The client's ID. + type: integer + example: 7 + login: + description: The login name of the client account owner. + type: string + example: owner + guid: + description: The global user ID of the client account just added to Plesk. + type: string + example: b623e93d-dc72-4102-b5f0-ded427cf0fb1 + external_id: + description: >- + The client GUID in the Plesk components (for example, Business + Manager). + type: string + example: b623e93d-dc72-4102-b5f0-ded427cf0fb1 + PlanReference: + type: object + description: >- + Identifies the service plan. One of the following properties is + required. + anyOf: + - title: id + required: + - id + - title: name + required: + - name + - title: guid + required: + - guid + - title: external_id + required: + - external_id + properties: + name: + description: Service plan name. + type: string + example: Unlimited + id: + description: Service plan ID. + type: integer + example: 7 + guid: + description: Service plan GUID. + type: string + example: b623e93d-dc72-4102-b5f0-ded427cf0fb1 + external_id: + description: Service plan external ID. + type: string + example: plan-ext-001 + DomainAlias: + type: object + properties: + id: + description: Domain alias ID. + type: integer + example: 123 + name: + description: Domain alias name. + type: string + example: example.com + ascii_name: + description: Domain alias name in ASCII. + type: string + example: ascii-example.com + web: + description: Web service status. + type: boolean + example: false + dns: + description: DNS service status. + type: boolean + example: true + mail: + description: Mail service status. + type: boolean + example: false + seo_redirect: + description: 301 redirect to HTTPS status. + type: boolean + example: true + required: + - id + - name + - ascii_name + - web + - dns + - mail + - seo_redirect + FtpUser: + type: object + properties: + id: + description: User ID in the database + type: integer + example: 2 + name: + description: User name in the system + type: string + example: exampleuser + home: + description: Subdirectory of the WWW Root that user is restricted to + type: string + example: /httpdocs + quota: + description: Hard disk quota in bytes (if supported by platform) + type: integer + example: -1 + permissions: + description: Access permissions of the user (if supported by platform) + type: object + properties: + write: + type: string + enum: + - 'true' + - 'false' + read: + type: string + enum: + - 'true' + - 'false' + parent_domain: + description: ID of the parent domain + type: integer + example: 4 + required: + - id + - name + - home + - parent_domain + FtpUserRequest: + type: object + properties: + name: + description: User name in the system + type: string + example: exampleuser + password: + description: User password + type: string + example: changeme1Q** + home: + description: Subdirectory of the WWW Root that user is restricted to + type: string + example: /httpdocs + quota: + description: Hard disk quota in bytes (if supported by platform) + type: integer + example: -1 + permissions: + description: Access permissions of the user (if supported by platform) + type: object + properties: + read: + type: string + enum: + - 'true' + - 'false' + write: + type: string + enum: + - 'true' + - 'false' + parent_domain: + $ref: '#/components/schemas/DomainReference' + required: + - name + - password + - parent_domain + FtpUserUpdateRequest: + type: object + properties: + name: + description: User name in the system + type: string + example: exampleuser + password: + description: User password + type: string + home: + description: Subdirectory of the WWW Root that user is restricted to + type: string + example: /httpdocs + quota: + description: Hard disk quota in bytes (if supported by platform) + type: integer + example: -1 + permissions: + description: Access permissions of the user (if supported by platform) + type: object + properties: + read: + type: string + enum: + - 'true' + - 'false' + write: + type: string + enum: + - 'true' + - 'false' + Database: + type: object + properties: + id: + description: ID of the database + type: integer + example: 2 + name: + description: Database name + type: string + example: exampledb + type: + description: Database server type + type: string + enum: + - mssql + - mysql + - postgresql + parent_domain: + description: ID of the parent domain + type: integer + example: 3 + server_id: + description: ID of the database server + type: integer + example: 1 + default_user_id: + description: ID of the default database user for database + type: integer + example: 6 + DatabaseRequest: + type: object + required: + - name + - parent_domain + properties: + name: + description: Database name + type: string + example: exampledb + type: + description: Database server type + type: string + enum: + - mssql + - mysql + - postgresql + parent_domain: + $ref: '#/components/schemas/DomainReference' + server_id: + description: ID of the database server + type: integer + example: 1 + DatabaseServer: + type: object + properties: + id: + description: ID of the database server + type: integer + example: 1 + host: + description: Database server host + type: string + example: 127.0.0.1 + port: + description: Database server port + type: integer + example: 3306 + type: + description: Database server type + type: string + enum: + - mssql + - mysql + - postgresql + status: + description: Database server status + type: string + db_count: + description: Database server database count + type: integer + example: 123 + is_default: + description: Flag that server is default for servers of this type + type: boolean + is_local: + description: Flag that server is installed on local service node + type: boolean + DatabaseUser: + type: object + properties: + id: + description: ID of the database user + type: integer + example: 5 + login: + description: Database user login + type: string + example: exampledbuser + database_id: + description: >- + ID of the database this user has access to. A value of 0 means the + user has access to all databases on the subscription (created with + server_id instead of database_id). + type: integer + example: 12 + DatabaseUserRequest: + type: object + description: >- + Either parent_domain and server_id together, or database_id must be + provided. + properties: + login: + description: Database user login + type: string + example: exampledbuser + password: + description: User password + type: string + example: changeme1Q** + parent_domain: + $ref: '#/components/schemas/DomainReference' + server_id: + description: >- + ID of the database server (if user should have access to all + databases on subscription) + type: integer + example: 9 + database_id: + description: ID of the database (if user should have access to one database) + type: integer + example: 3 + required: + - login + - password + anyOf: + - title: parent_domain and server_id + required: + - parent_domain + - server_id + - title: database_id + required: + - database_id + DatabaseUserUpdateRequest: + type: object + properties: + login: + description: Database user login + type: string + example: exampledbuser + password: + description: New user password + type: string + example: changeme1Q** + DnsRecord: + type: object + properties: + id: + description: Unique identifier of the DNS record + type: integer + type: + description: The type of the DNS record + type: string + enum: + - A + - CNAME + - PTR + - MX + - NS + - TXT + - SOA + - AXFR + - SRV + - AAAA + - DS + - CAA + - TLSA + - HTTPS + host: + description: The IP address or name of a host, that will be used by DNS + type: string + value: + description: The value that will be linked with the host value + type: string + opt: + description: Optional information about the DNS record + type: string + ttl: + description: >- + The amount of time (in seconds) that slave DNS servers should store + the record in a cache + type: integer + DnsRecordRequest: + type: object + properties: + type: + description: The type of the DNS record + type: string + enum: + - A + - CNAME + - PTR + - MX + - NS + - TXT + - SOA + - AXFR + - SRV + - AAAA + - DS + - CAA + - TLSA + - HTTPS + host: + description: The IP address or name of a host, that will be used by DNS + type: string + value: + description: The value that will be linked with the host value + type: string + opt: + description: Optional information about the DNS record + type: string + ttl: + description: >- + The amount of time (in seconds) that slave DNS servers should store + the record in a cache + type: integer + FileContentResponse: + type: object + properties: + encoding: + description: Content transfer encoding of the "content" field. + type: string + enum: + - plain + - base64 + example: plain + content: + description: File contents encoded according to "encoding". + type: string + example: hello world + required: + - encoding + - content + FileChmodRequest: + type: object + properties: + mode: + description: >- + Octal permission mode (4 digits, e.g. "0755"). Only the low nine + permission bits (owner/group/other) are accepted; the leading digit + must be "0". Setuid, setgid, and sticky bits are not allowed. + type: string + pattern: ^0[0-7]{3}$ + example: '0755' + required: + - mode + FileCopyMoveRequest: + type: object + properties: + source: + description: > + Source path. A leading "/" is treated as an absolute filesystem + path; + + otherwise the path is resolved relative to the domain's document + root. + type: string + example: old-name.txt + destination: + description: > + Destination path. A leading "/" is treated as an absolute filesystem + path; + + otherwise the path is resolved relative to the domain's document + root. + type: string + example: new-name.txt + overwrite: + description: Whether to overwrite the destination if it already exists. + type: boolean + default: false + required: + - source + - destination + FileContentRequest: + type: object + properties: + encoding: + description: Content transfer encoding of the "content" field. + type: string + enum: + - plain + - base64 + example: base64 + content: + description: >- + File contents encoded according to "encoding". May be empty to write + an empty file. + type: string + example: SGVsbG8sIHdvcmxkIQ== + required: + - encoding + - content + File: + type: object + properties: + name: + description: Entry basename. + type: string + example: index.html + path: + description: >- + Path relative to the domain's document root. Can be sent back as the + `path` query parameter. + type: string + example: index.html + fullpath: + description: Absolute filesystem path. + type: string + example: /var/www/vhosts/example.com/httpdocs/index.html + type: + description: Filesystem entry type. + type: string + enum: + - file + - directory + example: file + size: + description: Entry size in bytes. + type: integer + format: int64 + example: 1024 + mtime: + description: Last modification time as Unix timestamp. + type: integer + example: 1715587200 + mode: + description: Octal permission mode (4 digits). + type: string + example: '0644' + required: + - name + - path + - fullpath + - type + - size + - mtime + - mode diff --git a/routes/api.php b/routes/api.php index bacc7aa..bd0d1c2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -604,6 +604,7 @@ Route::prefix('v1') Route::post('sync', 'sync')->middleware('perm:module-vcenter.manager'); Route::post('import', 'import')->middleware('perm:module-vcenter.manager'); Route::delete('bulk', 'bulkDelete')->middleware('perm:module-vcenter.manager'); + Route::post('bulk-update-gateway', 'bulkUpdateGateway')->middleware('perm:module-vcenter.manager'); Route::get('{id}', 'show')->middleware('perm:module-vcenter.view'); Route::put('{id}', 'update')->middleware('perm:module-vcenter.manager'); Route::delete('{id}', 'destroy')->middleware('perm:module-vcenter.manager');