feat: Implement Plesk domain and hosting services
- Added PleskDomainService for managing domain operations (create, update, remove, get info). - Introduced PleskHostingService for provisioning hosting accounts and notifying customers. - Created PleskService for handling API requests to Plesk. - Added migration to include 'type' column in dns_manager_servers table. - Created migration for module_hosting_accounts table to track hosting subscriptions. - Created migration for module_hosting_plesk_clients table to manage Plesk client credentials. - Added email templates for notifying customers and admins about hosting account creation.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Server;
|
||||
|
||||
enum ServerType: string
|
||||
{
|
||||
case DOMAIN = 'domain';
|
||||
case HOSTING = 'hosting';
|
||||
case BOTH = 'both';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DOMAIN => 'Domain / DNS',
|
||||
self::HOSTING => 'Hosting',
|
||||
self::BOTH => 'Domain & Hosting',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ class ServerController extends Controller
|
||||
'password' => 'required|string|max:255',
|
||||
'api_key' => 'nullable|string',
|
||||
'status' => 'nullable|string|in:activated,Stopped',
|
||||
'type' => 'nullable|string|in:domain,hosting,both',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
@@ -86,6 +87,7 @@ class ServerController extends Controller
|
||||
'password' => Crypt::encryptString($validated['password']),
|
||||
'api_key' => $validated['api_key'] ?? null,
|
||||
'status' => $validated['status'] ?? 'activated',
|
||||
'type' => $validated['type'] ?? 'domain',
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
@@ -116,6 +118,7 @@ class ServerController extends Controller
|
||||
'password' => 'nullable|string|max:255',
|
||||
'api_key' => 'nullable|string',
|
||||
'status' => 'nullable|string|in:activated,Stopped',
|
||||
'type' => 'nullable|string|in:domain,hosting,both',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
@@ -182,6 +185,7 @@ class ServerController extends Controller
|
||||
'username' => $server->username,
|
||||
'api_key' => $server->api_key,
|
||||
'status' => $server->status,
|
||||
'type' => $server->type,
|
||||
'created_at' => $server->created_at,
|
||||
'updated_at' => $server->updated_at,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Hosting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ModuleHosting\CreateHostingAccountRequest;
|
||||
use App\Http\Requests\ModuleHosting\UpdateHostingAccountRequest;
|
||||
use App\Http\Resources\Api\Admin\HostingAccountResource;
|
||||
use App\Repositories\Module\HostingAccount\HostingAccountRepositoryInterface;
|
||||
use App\Services\Plesk\PleskHostingService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HostingAccountController extends Controller
|
||||
{
|
||||
private const RELATIONSHIPS = ['customer', 'server', 'domain'];
|
||||
|
||||
public function __construct(
|
||||
private readonly HostingAccountRepositoryInterface $accounts,
|
||||
private readonly PleskHostingService $pleskHostingService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->get('per_page', 15), 500);
|
||||
$filter = $request->get('filter');
|
||||
$status = $request->get('status');
|
||||
$customerId = $request->get('customer_id');
|
||||
|
||||
$query = $this->accounts->model
|
||||
->with(self::RELATIONSHIPS)
|
||||
->newQuery();
|
||||
|
||||
if ($filter) {
|
||||
$query->where(function ($sub) use ($filter) {
|
||||
$sub->where('hosting_plan', 'like', "%{$filter}%")
|
||||
->orWhere('ftp_username', 'like', "%{$filter}%")
|
||||
->orWhere('system_username', 'like', "%{$filter}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($status !== null && $status !== '') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if ($customerId) {
|
||||
$query->where('customer_id', $customerId);
|
||||
}
|
||||
|
||||
$paginator = $query->orderByDesc('id')->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => HostingAccountResource::collection($paginator->getCollection()),
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$account = $this->accounts->model->with(self::RELATIONSHIPS)->find($id);
|
||||
|
||||
if (!$account) {
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy hosting account #$id"], 404);
|
||||
}
|
||||
|
||||
return response()->json(['error' => false, 'data' => new HostingAccountResource($account)]);
|
||||
}
|
||||
|
||||
public function store(CreateHostingAccountRequest $request): JsonResponse
|
||||
{
|
||||
$result = $this->pleskHostingService->provision($request->validated());
|
||||
|
||||
if ($result['error']) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $result['message'] ?? 'Không thể tạo hosting account trên Plesk',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$account = $result['data']->load(self::RELATIONSHIPS);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => new HostingAccountResource($account),
|
||||
// Not persisted anywhere — this response is the only place it's ever shown.
|
||||
'ftp_password' => $result['ftp_password'],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(int $id, UpdateHostingAccountRequest $request): JsonResponse
|
||||
{
|
||||
$account = $this->accounts->find($id);
|
||||
|
||||
if (!$account) {
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy hosting account #$id"], 404);
|
||||
}
|
||||
|
||||
$this->accounts->update($id, $request->validated());
|
||||
|
||||
return $this->show($id);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$account = $this->accounts->find($id);
|
||||
|
||||
if (!$account) {
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy hosting account #$id"], 404);
|
||||
}
|
||||
|
||||
// Local record only — does not deprovision the subscription on Plesk,
|
||||
// same as DnsManager\DomainController::destroy leaving the Plesk side alone.
|
||||
$this->accounts->delete($id);
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã xoá hosting account']);
|
||||
}
|
||||
|
||||
public function suspend(int $id): JsonResponse
|
||||
{
|
||||
return $this->notImplemented();
|
||||
}
|
||||
|
||||
public function unsuspend(int $id): JsonResponse
|
||||
{
|
||||
return $this->notImplemented();
|
||||
}
|
||||
|
||||
public function resetFtpPassword(int $id): JsonResponse
|
||||
{
|
||||
return $this->notImplemented();
|
||||
}
|
||||
|
||||
private function notImplemented(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Not implemented in this phase',
|
||||
], 501);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ModuleHosting;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateHostingAccountRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'customer_id' => 'required|integer|exists:customers,id',
|
||||
'dns_manager_server_id' => 'required|integer|exists:dns_manager_servers,id',
|
||||
'dns_manager_domain_id' => 'nullable|integer|exists:dns_manager_domain,id',
|
||||
'service_id' => 'nullable|integer|exists:services,id',
|
||||
'domain_name' => 'required|string|max:255',
|
||||
'hosting_plan' => 'required|string|max:100',
|
||||
'ftp_password' => 'nullable|string|min:8|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\ModuleHosting;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateHostingAccountRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* Phase 1 only re-links local metadata — it does not call Plesk, so
|
||||
* fields that would require re-provisioning (server, domain name,
|
||||
* plan) are not editable here.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'dns_manager_domain_id' => 'nullable|integer|exists:dns_manager_domain,id',
|
||||
'service_id' => 'nullable|integer|exists:services,id',
|
||||
'hosting_plan' => 'sometimes|required|string|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Api\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class HostingAccountResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'customer_id' => $this->customer_id,
|
||||
'customer_name' => $this->whenLoaded('customer', fn () => $this->customer?->fullname),
|
||||
'service_id' => $this->service_id,
|
||||
'dns_manager_server_id' => $this->dns_manager_server_id,
|
||||
'server_name' => $this->whenLoaded('server', fn () => $this->server?->name),
|
||||
'dns_manager_domain_id' => $this->dns_manager_domain_id,
|
||||
'domain_name' => $this->whenLoaded('domain', fn () => $this->domain?->domain_name),
|
||||
'plesk_client_id' => $this->plesk_client_id,
|
||||
'plesk_subscription_id' => $this->plesk_subscription_id,
|
||||
'plesk_guid' => $this->plesk_guid,
|
||||
'hosting_plan' => $this->hosting_plan,
|
||||
'ftp_username' => $this->ftp_username,
|
||||
'system_username' => $this->system_username,
|
||||
'document_root' => $this->document_root,
|
||||
'status' => $this->status,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\Mail\Admin;
|
||||
|
||||
use App\Mail\Admin\HostingAccountCreatedAdminNotification;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class HostingAccountCreatedAdminNotificationJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $timeout = 60;
|
||||
|
||||
public function __construct(public readonly array $data) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
|
||||
$adminEmail = $settingValue['admin_email'] ?? null;
|
||||
|
||||
if (empty($adminEmail)) {
|
||||
Log::warning('HostingAccountCreatedAdminNotificationJob: admin_email not configured in settings.');
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = array_merge($this->data, [
|
||||
'company_name' => $settingValue['company'] ?? 'Portal Admin',
|
||||
]);
|
||||
|
||||
try {
|
||||
Mail::to($adminEmail)->send(new HostingAccountCreatedAdminNotification($payload));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('HostingAccountCreatedAdminNotificationJob: failed to send email. ' . $e->getMessage(), [
|
||||
'admin_email' => $adminEmail,
|
||||
'domain_name' => $this->data['domain_name'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('HostingAccountCreatedAdminNotificationJob: job failed.', [
|
||||
'error' => $e->getMessage(),
|
||||
'domain_name' => $this->data['domain_name'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Mail\Customer;
|
||||
|
||||
use App\Mail\Customer\HostingAccountCreated;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class HostingAccountCreatedJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public array $data;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (empty($this->data['customer_email'])) {
|
||||
Log::warning('HostingAccountCreatedJob: missing customer_email, skipped.', [
|
||||
'domain_name' => $this->data['domain_name'] ?? null,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Mail::to($this->data['customer_email'])
|
||||
->send(new HostingAccountCreated($this->data));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Không thể gửi email thông báo khởi tạo hosting: ' . $e->getMessage(), [
|
||||
'customer_email' => $this->data['customer_email'] ?? null,
|
||||
'domain_name' => $this->data['domain_name'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('HostingAccountCreatedJob: job failed.', [
|
||||
'error' => $e->getMessage(),
|
||||
'domain_name' => $this->data['domain_name'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class HostingAccountCreatedAdminNotification extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public readonly array $data) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[ADMIN] Hosting account mới — ' . ($this->data['domain_name'] ?? 'N/A'),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.admin.hosting-account-created',
|
||||
with: $this->data,
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail\Customer;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class HostingAccountCreated extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public array $data) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$appName = $this->payload()['company_name'] ?? config('app.name', 'The Gioi So');
|
||||
|
||||
return new Envelope(
|
||||
subject: 'Hosting của bạn đã sẵn sàng — ' . ($this->data['domain_name'] ?? '') . ' — ' . $appName,
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
$data = $this->payload();
|
||||
|
||||
return new Content(
|
||||
view: 'emails.customer.hosting-account-created',
|
||||
with: [
|
||||
'appName' => $data['company_name'] ?? config('app.name', 'The Gioi So'),
|
||||
'fullName' => $data['customer_name'] ?? 'Quý khách',
|
||||
'domainName' => $data['domain_name'] ?? '',
|
||||
'hostingPlan' => $data['hosting_plan'] ?? '',
|
||||
'serverHost' => $data['server_host'] ?? '',
|
||||
'ftpUsername' => $data['ftp_username'] ?? '',
|
||||
'ftpPassword' => $data['ftp_password'] ?? '',
|
||||
'pleskLogin' => $data['plesk_login'] ?? '',
|
||||
'pleskPassword' => $data['plesk_password'] ?? '',
|
||||
'pleskPanelUrl' => $data['plesk_panel_url'] ?? '',
|
||||
'loginUrl' => $data['login_url'] ?? rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap',
|
||||
'supportEmail' => $data['support_email'] ?? config('mail.support_email', 'support@thegioiso.com'),
|
||||
'supportPhone' => $data['support_phone'] ?? config('mail.support_phone', '1900.xxxx'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function payload(): array
|
||||
{
|
||||
try {
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
} catch (\Throwable) {
|
||||
$settingValue = [];
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'company_name' => $settingValue['company'] ?? config('app.name', 'The Gioi So'),
|
||||
'support_email' => $settingValue['email'] ?? config('mail.support_email', 'support@thegioiso.com'),
|
||||
'support_phone' => $settingValue['hotline'] ?? ($settingValue['phone'] ?? config('mail.support_phone', '1900.xxxx')),
|
||||
'login_url' => rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap',
|
||||
], $this->data);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Server\ServerType;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -19,6 +20,7 @@ class DnsServer extends Model
|
||||
'username',
|
||||
'api_key',
|
||||
'status',
|
||||
'type',
|
||||
];
|
||||
|
||||
public function getStatus(): string
|
||||
@@ -30,6 +32,13 @@ class DnsServer extends Model
|
||||
};
|
||||
}
|
||||
|
||||
public function servesHosting(): bool
|
||||
{
|
||||
$type = $this->attributes['type'] ?? ServerType::DOMAIN->value;
|
||||
|
||||
return in_array($type, [ServerType::HOSTING->value, ServerType::BOTH->value], true);
|
||||
}
|
||||
|
||||
protected function scopeIsActive($query)
|
||||
{
|
||||
return $query->where('status', 'activated');
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ModuleHostingPleskClient extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'module_hosting_plesk_clients';
|
||||
|
||||
protected $fillable = [
|
||||
'customer_id',
|
||||
'dns_manager_server_id',
|
||||
'plesk_client_id',
|
||||
'login',
|
||||
'password',
|
||||
];
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(DnsServer::class, 'dns_manager_server_id');
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,8 @@ use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingR
|
||||
|
||||
use App\Repositories\Module\HostingServer\HostingServerCachedRepository;
|
||||
use App\Repositories\Module\HostingServer\HostingServerRepositoryInterface;
|
||||
use App\Repositories\Module\HostingAccount\HostingAccountCachedRepository;
|
||||
use App\Repositories\Module\HostingAccount\HostingAccountRepositoryInterface;
|
||||
use App\Repositories\Module\IpAddress\IpAddressCachedRepository;
|
||||
use App\Repositories\Module\IpAddress\IpAddressRepositoryInterface;
|
||||
use App\Repositories\Module\Network\NetworkCachedRepository;
|
||||
@@ -198,6 +200,8 @@ class RepositoryProvider extends ServiceProvider
|
||||
$this->app->singleton(ModuleHostingRepositoryInterface::class, ModuleHostingCachedRepository::class);
|
||||
//Hosting Server
|
||||
$this->app->singleton(HostingServerRepositoryInterface::class, HostingServerCachedRepository::class);
|
||||
//Hosting Account
|
||||
$this->app->singleton(HostingAccountRepositoryInterface::class, HostingAccountCachedRepository::class);
|
||||
//IpAddress
|
||||
$this->app->singleton(IpAddressRepositoryInterface::class, IpAddressCachedRepository::class);
|
||||
//Network
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Module\HostingAccount;
|
||||
|
||||
use App\Repositories\BaseCachedRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class HostingAccountCachedRepository extends BaseCachedRepository implements HostingAccountRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
public HostingAccountRepository $hostingAccountRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getModel()
|
||||
{
|
||||
return $this->hostingAccountRepository->getModel();
|
||||
}
|
||||
|
||||
public function filter(
|
||||
array $data = [],
|
||||
array $relationships = [],
|
||||
array $withCount = [],
|
||||
array $select = ['*'],
|
||||
bool $returnQuery = false
|
||||
) {
|
||||
if (
|
||||
$returnQuery ||
|
||||
!empty($data['paginate'])
|
||||
) {
|
||||
return $this->hostingAccountRepository
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery);
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filter' => compact('data', 'relationships', 'withCount', 'select')
|
||||
])));
|
||||
|
||||
return Cache::remember(
|
||||
$cacheKey,
|
||||
$this->cacheTime,
|
||||
fn() =>
|
||||
$this->hostingAccountRepository
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Module\HostingAccount;
|
||||
|
||||
use App\Models\ModuleHostingAccount;
|
||||
use App\Repositories\BaseRepository;
|
||||
|
||||
class HostingAccountRepository extends BaseRepository implements HostingAccountRepositoryInterface
|
||||
{
|
||||
public function getModel()
|
||||
{
|
||||
return ModuleHostingAccount::class;
|
||||
}
|
||||
|
||||
public function filter(
|
||||
array $data = [],
|
||||
array $relationships = [],
|
||||
array $withCount = [],
|
||||
array $select = ['*'],
|
||||
bool $returnQuery = false
|
||||
) {
|
||||
$query = $this->model
|
||||
->select($select)
|
||||
->with($relationships);
|
||||
|
||||
if (!empty($data['customer_id'])) {
|
||||
$query->where('customer_id', $data['customer_id']);
|
||||
}
|
||||
|
||||
if (!empty($data['status'])) {
|
||||
$query->where('status', $data['status']);
|
||||
}
|
||||
|
||||
if (!empty($withCount)) {
|
||||
$query->withCount($withCount);
|
||||
}
|
||||
|
||||
if ($returnQuery) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if (!empty($data['paginate'])) {
|
||||
return $query->paginate(
|
||||
$data['per_page'] ?? 15
|
||||
);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Module\HostingAccount;
|
||||
|
||||
use App\Repositories\BaseRepositoryInterface;
|
||||
|
||||
interface HostingAccountRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
public function filter(
|
||||
array $data = [],
|
||||
array $relationships = [],
|
||||
array $withCount = [],
|
||||
array $select = ['*'],
|
||||
bool $returnQuery = false
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Plesk;
|
||||
|
||||
use App\Http\Facade\HelperFacade as Helper;
|
||||
use App\Models\Customer;
|
||||
use App\Models\DnsServer;
|
||||
use App\Models\ModuleHostingPleskClient;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Plesk `/clients` entry point — the customer's own Plesk Panel login,
|
||||
* separate from the FTP/hosting credential PleskDomainService hands out.
|
||||
* Plesk never returns a client's password after creation, so it's stored
|
||||
* (encrypted) in module_hosting_plesk_clients — the only way to ever show
|
||||
* it again on reuse.
|
||||
*/
|
||||
class PleskClientService
|
||||
{
|
||||
public function __construct(private PleskService $plesk)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{error: bool, data?: array{id: int, login: string, password: ?string}, status?: int, message?: string}
|
||||
*/
|
||||
public function findOrCreateCustomer(DnsServer $server, Customer $customer): array
|
||||
{
|
||||
$existing = ModuleHostingPleskClient::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('dns_manager_server_id', $server->id)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return [
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'id' => $existing->plesk_client_id,
|
||||
'login' => $existing->login,
|
||||
'password' => Crypt::decryptString($existing->password),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$login = $this->generateUsername($customer);
|
||||
$password = Helper::general_password();
|
||||
|
||||
$created = $this->createCustomer($server, $customer, $login, $password);
|
||||
|
||||
if (!$created['error']) {
|
||||
$this->persist($customer, $server, $created['data']['id'], $login, $password);
|
||||
|
||||
return ['error' => false, 'data' => ['id' => $created['data']['id'], 'login' => $login, 'password' => $password]];
|
||||
}
|
||||
|
||||
// login already exists on this Plesk box but we have no local record of it (created
|
||||
// out-of-band, or our own table was reset) — Plesk never gives back the old password,
|
||||
// so reset it to one we generate now and persist that, instead of losing it again.
|
||||
if (in_array($created['status'] ?? null, [400, 409], true)) {
|
||||
$existingRemote = $this->findByLogin($server, $login);
|
||||
|
||||
if ($existingRemote) {
|
||||
$reset = $this->resetPassword($server, (int) $existingRemote['id'], $password);
|
||||
|
||||
if ($reset) {
|
||||
$this->persist($customer, $server, (int) $existingRemote['id'], $login, $password);
|
||||
|
||||
return ['error' => false, 'data' => ['id' => (int) $existingRemote['id'], 'login' => $login, 'password' => $password]];
|
||||
}
|
||||
|
||||
Log::error('Plesk client found but password reset failed — reusing without a known password', [
|
||||
'plesk_client_id' => $existingRemote['id'],
|
||||
'login' => $login,
|
||||
]);
|
||||
|
||||
return ['error' => false, 'data' => ['id' => (int) $existingRemote['id'], 'login' => $login, 'password' => null]];
|
||||
}
|
||||
}
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
private function createCustomer(DnsServer $server, Customer $customer, string $login, string $password): array
|
||||
{
|
||||
$param = [
|
||||
'name' => $customer->fullname ?: $login,
|
||||
'login' => $login,
|
||||
'email' => $customer->email,
|
||||
'password' => $password,
|
||||
'type' => 'customer',
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $this->plesk->client($server, 30)
|
||||
->post($this->plesk->url($server, 'clients'), $param);
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if ($response->successful() && isset($data['id'], $data['guid'])) {
|
||||
return ['error' => false, 'data' => $data];
|
||||
}
|
||||
|
||||
Log::error('Plesk client creation failed', ['status' => $response->status(), 'response' => $data]);
|
||||
|
||||
return [
|
||||
'error' => true,
|
||||
'status' => $response->status(),
|
||||
'message' => $data['message'] ?? 'Plesk client creation failed',
|
||||
];
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk client creation failed', ['error' => $e->getMessage(), 'response' => $e->response?->body()]);
|
||||
|
||||
return ['error' => true, 'status' => $e->response?->status(), 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPassword(DnsServer $server, int $pleskClientId, string $password): bool
|
||||
{
|
||||
try {
|
||||
$response = $this->plesk->client($server, 15)
|
||||
->put($this->plesk->url($server, 'clients', "/{$pleskClientId}"), ['password' => $password]);
|
||||
|
||||
return $response->successful();
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk client password reset failed', ['error' => $e->getMessage(), 'plesk_client_id' => $pleskClientId]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function findByLogin(DnsServer $server, string $login): ?array
|
||||
{
|
||||
try {
|
||||
$response = $this->plesk->client($server, 15)->get($this->plesk->url($server, 'clients'));
|
||||
|
||||
if (!$response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($response->json() ?? [] as $client) {
|
||||
if (($client['login'] ?? null) === $login) {
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plesk `login` is a short username, not the email (email is its own field
|
||||
* on the same payload). Derived from the email's local part + customer id
|
||||
* so it's stable and never collides across customers — needed because a
|
||||
* 409 on create is treated as "this customer already has a client".
|
||||
*/
|
||||
private function generateUsername(Customer $customer): string
|
||||
{
|
||||
$localPart = strtolower((string) strstr($customer->email, '@', true) ?: 'user');
|
||||
$sanitized = trim((string) preg_replace('/[^a-z0-9]+/', '_', $localPart), '_') ?: 'user';
|
||||
|
||||
return substr($sanitized, 0, 32) . '_' . $customer->id;
|
||||
}
|
||||
|
||||
private function persist(Customer $customer, DnsServer $server, int $pleskClientId, string $login, string $password): void
|
||||
{
|
||||
ModuleHostingPleskClient::query()->updateOrCreate(
|
||||
['customer_id' => $customer->id, 'dns_manager_server_id' => $server->id],
|
||||
['plesk_client_id' => $pleskClientId, 'login' => $login, 'password' => Crypt::encryptString($password)]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Plesk;
|
||||
|
||||
use App\Models\DnsServer;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Plesk `/domains` entry point — shared by the DNS module (ServerService)
|
||||
* and the Hosting module (PleskHostingService). Extracted from
|
||||
* ServerService so both stop drifting apart on the same Plesk call.
|
||||
*/
|
||||
class PleskDomainService
|
||||
{
|
||||
private ?string $lastError = null;
|
||||
|
||||
public function __construct(private PleskService $plesk)
|
||||
{
|
||||
}
|
||||
|
||||
/** Message from the most recent failed call — null if the last call succeeded or hasn't run yet. */
|
||||
public function getLastError(): ?string
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
public function createDomain(DnsServer $server, array $data = []): array|false
|
||||
{
|
||||
$this->lastError = null;
|
||||
|
||||
if (empty($data['domain_name'])) {
|
||||
$this->lastError = 'Domain name is required';
|
||||
return false;
|
||||
}
|
||||
|
||||
$param = [
|
||||
'name' => $data['domain_name'],
|
||||
'description' => '',
|
||||
'hosting_type' => 'virtual',
|
||||
'hosting_settings' => [
|
||||
'ftp_login' => $data['ftp_login'] ?? str_replace('.', '_', $data['domain_name']),
|
||||
'ftp_password' => $data['ftp_password'] ?? Str::random(12),
|
||||
],
|
||||
'plan' => ['name' => $data['plan'] ?? 'Default Domain'],
|
||||
];
|
||||
|
||||
// Omit ip_addresses/ipv4 unless explicitly given — Plesk assigns its
|
||||
// own default IP for the domain, and a wrong hardcoded IP here gets
|
||||
// rejected outright ("IP address 'x.x.x.x' is not available").
|
||||
if (!empty($data['ip_addresses'])) {
|
||||
$param['ip_addresses'] = $data['ip_addresses'];
|
||||
$param['ipv4'] = $data['ip_addresses'];
|
||||
}
|
||||
|
||||
if (!empty($data['owner_client_id'])) {
|
||||
$param['owner_client'] = ['id' => $data['owner_client_id']];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->plesk->client($server, 30)
|
||||
->post($this->plesk->url($server, 'domains'), $param);
|
||||
|
||||
$responseData = $response->json();
|
||||
|
||||
Log::info('Response Plesk API Created', ['data' => $responseData]);
|
||||
|
||||
if ($response->successful() && isset($responseData['id'], $responseData['guid'])) {
|
||||
return $responseData + ['hosting_settings' => $param['hosting_settings']];
|
||||
}
|
||||
|
||||
$this->lastError = $responseData['message'] ?? 'Plesk domain creation failed';
|
||||
} catch (RequestException $e) {
|
||||
$this->lastError = $e->response?->json('message') ?? $e->getMessage();
|
||||
Log::error('Plesk domain creation failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'response' => $e->response?->body(),
|
||||
'data' => $param,
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updateDomain(DnsServer $server, int $pleskDomainId, array $data = []): array|false
|
||||
{
|
||||
$this->lastError = null;
|
||||
|
||||
if (empty($pleskDomainId) || empty($data)) {
|
||||
$this->lastError = 'Invalid request';
|
||||
return false;
|
||||
}
|
||||
|
||||
$param = [
|
||||
'name' => $data['domain_name'],
|
||||
'description' => '',
|
||||
'hosting_type' => 'virtual',
|
||||
'hosting_settings' => [
|
||||
'ftp_login' => $data['ftp_login'] ?? str_replace('.', '_', $data['domain_name']),
|
||||
'ftp_password' => $data['ftp_password'] ?? Str::random(12),
|
||||
],
|
||||
'plan' => ['name' => $data['plan'] ?? 'Default Domain'],
|
||||
];
|
||||
|
||||
if (!empty($data['ip_addresses'])) {
|
||||
$param['ip_addresses'] = $data['ip_addresses'];
|
||||
$param['ipv4'] = $data['ip_addresses'];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->plesk->client($server, 30)
|
||||
->put($this->plesk->url($server, 'domains', "/{$pleskDomainId}"), $param);
|
||||
|
||||
$responseData = $response->json();
|
||||
|
||||
Log::info('Response Plesk API Updated', ['data' => $responseData]);
|
||||
|
||||
if ($response->successful() && isset($responseData['id'], $responseData['guid'])) {
|
||||
return $responseData;
|
||||
}
|
||||
|
||||
$this->lastError = $responseData['message'] ?? 'Plesk domain update failed';
|
||||
} catch (RequestException $e) {
|
||||
$this->lastError = $e->response?->json('message') ?? $e->getMessage();
|
||||
Log::error('Plesk domain update failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'response' => $e->response?->body(),
|
||||
'data' => $param,
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function removeDomain(DnsServer $server, int $pleskDomainId): bool
|
||||
{
|
||||
try {
|
||||
$response = $this->plesk->client($server, 30)
|
||||
->delete($this->plesk->url($server, 'domains', "/{$pleskDomainId}"));
|
||||
|
||||
if ($response->successful()) {
|
||||
Log::info('Xoá domain thành công', ['plesk_id' => $pleskDomainId, 'status' => $response->status()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::error('Xoá domain thất bại', [
|
||||
'plesk_id' => $pleskDomainId,
|
||||
'status' => $response->status(),
|
||||
'response' => $response->body(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Exception khi xoá domain', ['plesk_id' => $pleskDomainId, 'error' => $e->getMessage()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getDomainInfo(DnsServer $server, string $domain = ''): array|false
|
||||
{
|
||||
if (empty($domain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->plesk->client($server, 10)
|
||||
->get($this->plesk->url($server, 'domains'), ['name' => $domain]);
|
||||
|
||||
if ($response->successful()) {
|
||||
foreach ($response->json() ?? [] as $domainInfo) {
|
||||
if (isset($domainInfo['name']) && $domainInfo['name'] === $domain) {
|
||||
return $domainInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Plesk;
|
||||
|
||||
use App\Http\Facade\HelperFacade as Helper;
|
||||
use App\Jobs\Mail\Admin\HostingAccountCreatedAdminNotificationJob;
|
||||
use App\Jobs\Mail\Customer\HostingAccountCreatedJob;
|
||||
use App\Models\Customer;
|
||||
use App\Models\DnsServer;
|
||||
use App\Models\ModuleHostingAccount;
|
||||
use App\Repositories\DnsManager\Domain\DnsDomainRepositoryInterface;
|
||||
use App\Repositories\Module\HostingAccount\HostingAccountRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Phase 1 orchestration: Create Customer -> Create Hosting Subscription ->
|
||||
* Assign Plan -> persist module_hosting_accounts. No database/SSL/mail/git
|
||||
* deploy — that's later phases.
|
||||
*/
|
||||
class PleskHostingService
|
||||
{
|
||||
public function __construct(
|
||||
private PleskClientService $clients,
|
||||
private PleskDomainService $domains,
|
||||
private HostingAccountRepositoryInterface $accounts,
|
||||
private DnsDomainRepositoryInterface $dnsDomains,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provision(array $data): array
|
||||
{
|
||||
$server = DnsServer::find($data['dns_manager_server_id'] ?? null);
|
||||
|
||||
if (!$server) {
|
||||
return ['error' => true, 'message' => 'Hosting server not found'];
|
||||
}
|
||||
|
||||
if (!$server->servesHosting()) {
|
||||
return ['error' => true, 'message' => 'This server does not support hosting'];
|
||||
}
|
||||
|
||||
$customer = Customer::find($data['customer_id'] ?? null);
|
||||
|
||||
if (!$customer) {
|
||||
return ['error' => true, 'message' => 'Customer not found'];
|
||||
}
|
||||
|
||||
$client = $this->clients->findOrCreateCustomer($server, $customer);
|
||||
|
||||
if ($client['error']) {
|
||||
return $client;
|
||||
}
|
||||
|
||||
$pleskClientId = $client['data']['id'];
|
||||
$pleskLogin = $client['data']['login'];
|
||||
$pleskPassword = $client['data']['password'];
|
||||
|
||||
$ftpLogin = str_replace('.', '_', $data['domain_name']);
|
||||
// Not persisted anywhere — the only record of it is this response, once.
|
||||
$ftpPassword = $data['ftp_password'] ?? Helper::general_password();
|
||||
|
||||
$subscription = $this->domains->createDomain($server, [
|
||||
'domain_name' => $data['domain_name'],
|
||||
'ftp_login' => $ftpLogin,
|
||||
'ftp_password' => $ftpPassword,
|
||||
'plan' => $data['hosting_plan'] ?? null,
|
||||
'owner_client_id' => $pleskClientId,
|
||||
]);
|
||||
|
||||
if (!$subscription) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => $this->domains->getLastError() ?? 'Failed to create Plesk hosting subscription',
|
||||
];
|
||||
}
|
||||
|
||||
$domainInfo = $this->domains->getDomainInfo($server, $data['domain_name']) ?: [];
|
||||
|
||||
$dnsManagerDomainId = $data['dns_manager_domain_id'] ?? null;
|
||||
|
||||
if (!$dnsManagerDomainId) {
|
||||
// Plesk's /domains object is the same entity the DNS module tracks —
|
||||
// creating a hosting subscription there already created the DNS zone
|
||||
// on Plesk, so mirror it locally instead of leaving it unlinked.
|
||||
$dnsDomain = $this->dnsDomains->model->firstOrCreate(
|
||||
[
|
||||
'domain_name' => $data['domain_name'],
|
||||
'dns_manager_server_id' => $server->id,
|
||||
],
|
||||
[
|
||||
'plesk_id' => $subscription['id'],
|
||||
'is_created' => 1,
|
||||
]
|
||||
);
|
||||
|
||||
$dnsManagerDomainId = $dnsDomain->id;
|
||||
}
|
||||
|
||||
$account = $this->accounts->create([
|
||||
'customer_id' => $customer->id,
|
||||
'service_id' => $data['service_id'] ?? null,
|
||||
'dns_manager_server_id' => $server->id,
|
||||
'dns_manager_domain_id' => $dnsManagerDomainId,
|
||||
'plesk_client_id' => $pleskClientId,
|
||||
'plesk_subscription_id' => $subscription['id'],
|
||||
'plesk_guid' => $subscription['guid'],
|
||||
'hosting_plan' => $data['hosting_plan'] ?? null,
|
||||
'ftp_username' => $subscription['hosting_settings']['ftp_login'] ?? $ftpLogin,
|
||||
'system_username' => $subscription['hosting_settings']['ftp_login'] ?? $ftpLogin,
|
||||
'document_root' => $domainInfo['www_root'] ?? null,
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$this->notify($customer, $server, $account, $data['domain_name'], $ftpPassword, $pleskLogin, $pleskPassword);
|
||||
|
||||
return ['error' => false, 'data' => $account, 'ftp_password' => $ftpPassword];
|
||||
}
|
||||
|
||||
private function notify(
|
||||
Customer $customer,
|
||||
DnsServer $server,
|
||||
ModuleHostingAccount $account,
|
||||
string $domainName,
|
||||
string $ftpPassword,
|
||||
string $pleskLogin,
|
||||
?string $pleskPassword,
|
||||
): void {
|
||||
$shared = [
|
||||
'customer_name' => $customer->fullname,
|
||||
'customer_email' => $customer->email,
|
||||
'domain_name' => $domainName,
|
||||
'hosting_plan' => $account->hosting_plan,
|
||||
'server_name' => $server->name,
|
||||
'ftp_username' => $account->ftp_username,
|
||||
'ftp_password' => $ftpPassword,
|
||||
'plesk_login' => $pleskLogin,
|
||||
'plesk_password' => $pleskPassword,
|
||||
// Plesk's actual login page — not the :8443 REST API entry point.
|
||||
'plesk_panel_url' => 'https://' . $server->ip_address . '/login_up.php',
|
||||
'created_at' => now()->format('d/m/Y H:i:s'),
|
||||
];
|
||||
|
||||
dispatch(new HostingAccountCreatedJob(array_merge($shared, [
|
||||
'server_host' => $server->ip_address,
|
||||
])));
|
||||
|
||||
dispatch(new HostingAccountCreatedAdminNotificationJob(array_merge($shared, [
|
||||
'server_ip' => $server->ip_address,
|
||||
'plesk_client_id' => $account->plesk_client_id,
|
||||
'plesk_subscription_id' => $account->plesk_subscription_id,
|
||||
])));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Plesk;
|
||||
|
||||
use App\Facades\BaseHelperFacade as BaseHelper;
|
||||
use App\Models\DnsServer;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PleskService
|
||||
{
|
||||
protected array $entryPoints = [
|
||||
'clients' => ':8443/api/v2/clients',
|
||||
'domains' => ':8443/api/v2/domains',
|
||||
];
|
||||
|
||||
public function client(DnsServer $server, int $timeout = 30): PendingRequest
|
||||
{
|
||||
return Http::withoutVerifying()
|
||||
->withBasicAuth($server->username, Crypt::decryptString($server->password))
|
||||
->timeout($timeout);
|
||||
}
|
||||
|
||||
public function url(DnsServer $server, string $entryPoint, string $suffix = ''): string
|
||||
{
|
||||
return BaseHelper::url($server->ip_address) . $this->entryPoints[$entryPoint] . $suffix;
|
||||
}
|
||||
}
|
||||
+16
-113
@@ -5,11 +5,11 @@ namespace App\Services;
|
||||
use App\Facades\BaseHelperFacade as BaseHelper;
|
||||
use App\Models\DnsServer;
|
||||
use App\Repositories\DnsManager\Server\DnsServerRepositoryInterface;
|
||||
use App\Services\Plesk\PleskDomainService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ServerService
|
||||
{
|
||||
@@ -33,7 +33,8 @@ class ServerService
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
public DnsServerRepositoryInterface $serverRepository
|
||||
public DnsServerRepositoryInterface $serverRepository,
|
||||
private PleskDomainService $pleskDomainService,
|
||||
) {
|
||||
$this->server = $this->getServer();
|
||||
}
|
||||
@@ -205,32 +206,15 @@ class ServerService
|
||||
|
||||
public function getDomainInfo(string $domain = ''): array|false
|
||||
{
|
||||
if (empty($domain)) {
|
||||
if (empty($domain) || empty($this->server)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->withBasicAuth($this->server->username, Crypt::decryptString($this->server->password))
|
||||
->timeout(10)
|
||||
->get(BaseHelper::url($this->server->ip_address) . $this->entry_points['domain'], ['name' => $domain]);
|
||||
$result = $this->pleskDomainService->getDomainInfo($this->server, $domain);
|
||||
|
||||
$this->resetServer();
|
||||
$this->resetServer();
|
||||
|
||||
if ($response->successful()) {
|
||||
$domains = $response->json();
|
||||
foreach ($domains as $domainInfo) {
|
||||
if (isset($domainInfo['name']) && $domainInfo['name'] === $domain) {
|
||||
return $domainInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function createDomain(array $data = []): array|false
|
||||
@@ -243,42 +227,11 @@ class ServerService
|
||||
$this->server = $this->serverRepository->find($data['dns_manager_server_id']);
|
||||
}
|
||||
|
||||
$param = [
|
||||
'name' => $data['domain_name'],
|
||||
'description' => '',
|
||||
'hosting_type' => 'virtual',
|
||||
'hosting_settings' => [
|
||||
'ftp_login' => str_replace('.', '_', $data['domain_name']),
|
||||
'ftp_password' => Str::random(12),
|
||||
],
|
||||
'ip_addresses' => ['103.42.58.124'],
|
||||
'ipv4' => ['103.42.58.124'],
|
||||
'plan' => ['name' => 'Default Domain'],
|
||||
];
|
||||
$result = $this->pleskDomainService->createDomain($this->server, $data);
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->withBasicAuth($this->server->username, Crypt::decryptString($this->server->password))
|
||||
->timeout(30)
|
||||
->post(BaseHelper::url($this->server->ip_address) . $this->entry_points['domain'], $param);
|
||||
$this->resetServer();
|
||||
|
||||
$this->resetServer();
|
||||
$responseData = $response->json();
|
||||
|
||||
Log::info('Response Plesk API Created', ['data' => $responseData]);
|
||||
|
||||
if ($response->successful() && isset($responseData['id'], $responseData['guid'])) {
|
||||
return $responseData;
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk domain creation failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'response' => $e->response?->body(),
|
||||
'data' => $param,
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function updateDomain(int $pleskDomainId, array $data = []): array|false
|
||||
@@ -291,42 +244,11 @@ class ServerService
|
||||
$this->server = $this->serverRepository->find($data['dns_manager_server_id']);
|
||||
}
|
||||
|
||||
$param = [
|
||||
'name' => $data['domain_name'],
|
||||
'description' => '',
|
||||
'hosting_type' => 'virtual',
|
||||
'hosting_settings' => [
|
||||
'ftp_login' => str_replace('.', '_', $data['domain_name']),
|
||||
'ftp_password' => Str::random(12),
|
||||
],
|
||||
'ip_addresses' => ['103.42.58.124'],
|
||||
'ipv4' => ['103.42.58.124'],
|
||||
'plan' => ['name' => 'Default Domain'],
|
||||
];
|
||||
$result = $this->pleskDomainService->updateDomain($this->server, $pleskDomainId, $data);
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->withBasicAuth($this->server->username, Crypt::decryptString($this->server->password))
|
||||
->timeout(30)
|
||||
->put(BaseHelper::url($this->server->ip_address) . $this->entry_points['domain'] . "/{$pleskDomainId}", $param);
|
||||
$this->resetServer();
|
||||
|
||||
$this->resetServer();
|
||||
$responseData = $response->json();
|
||||
|
||||
Log::info('Response Plesk API Updated', ['data' => $responseData]);
|
||||
|
||||
if ($response->successful() && isset($responseData['id'], $responseData['guid'])) {
|
||||
return $responseData;
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk domain update failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'response' => $e->response?->body(),
|
||||
'data' => $param,
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function removeDomain(int $pleskDomainId): bool
|
||||
@@ -335,30 +257,11 @@ class ServerService
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->withBasicAuth($this->server->username, Crypt::decryptString($this->server->password))
|
||||
->timeout(30)
|
||||
->delete(BaseHelper::url($this->server->ip_address) . $this->entry_points['domain'] . "/{$pleskDomainId}");
|
||||
$result = $this->pleskDomainService->removeDomain($this->server, $pleskDomainId);
|
||||
|
||||
$this->resetServer();
|
||||
$this->resetServer();
|
||||
|
||||
if ($response->successful()) {
|
||||
Log::info('Xoá domain thành công', ['plesk_id' => $pleskDomainId, 'status' => $response->status()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::error('Xoá domain thất bại', [
|
||||
'plesk_id' => $pleskDomainId,
|
||||
'status' => $response->status(),
|
||||
'response' => $response->body(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Exception khi xoá domain', ['plesk_id' => $pleskDomainId, 'error' => $e->getMessage()]);
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function addServerById(int $serverId): bool
|
||||
|
||||
Reference in New Issue
Block a user