From 0c260a7e1ca3eb6ee417dbb346538519ea0e526c Mon Sep 17 00:00:00 2001 From: pbao9 Date: Mon, 2 Mar 2026 10:26:01 +0700 Subject: [PATCH] feat: Implement VM restart, auto-renew invoice functionality, and client invoice detail API, alongside admin UI improvements. --- .../Api/Client/ClientController.php | 26 +- .../Api/Service/OrderController.php | 134 +++++ app/Http/Services/vCenter.php | 60 ++- app/Models/ModuleSnapshot.php | 3 + ...100000_add_auto_renew_to_invoice_table.php | 25 + .../administrator/layout/master2.blade.php | 96 ++++ .../administrator/pages/auth/login.blade.php | 30 +- routes/client.php | 487 ++++++++++++++++++ test-console-ticket.php | 25 - 9 files changed, 842 insertions(+), 44 deletions(-) create mode 100644 database/migrations/2026_03_02_100000_add_auto_renew_to_invoice_table.php create mode 100644 resources/views/administrator/layout/master2.blade.php delete mode 100644 test-console-ticket.php diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index 174471e..6f40260 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -8,7 +8,8 @@ use App\Http\Requests\Api\Client\ClientRegisterRequest; use App\Http\Requests\Api\Client\ClientUpdateRequest; use App\Http\Resources\Api\Client\ClientResource; use App\Services\Api\Client\ClientAuthService; -use Illuminate\Support\Facades\Auth; +use Tymon\JWTAuth\Facades\JWTAuth; +use Tymon\JWTAuth\Exceptions\JWTException; /** * @OA\Tag( @@ -230,4 +231,25 @@ class ClientController extends Controller ], ]); } -} \ No newline at end of file + + public function refresh() + { + try { + $newToken = JWTAuth::parseToken()->refresh(); + $user = JWTAuth::setToken($newToken)->toUser(); + + return response()->json([ + 'error' => false, + 'data' => [ + 'token' => $newToken, + 'client' => new ClientResource($user), + ], + ]); + } catch (JWTException $e) { + return response()->json([ + 'error' => true, + 'message' => 'Không thể làm mới phiên đăng nhập. Vui lòng đăng nhập lại.', + ], 401); + } + } +} diff --git a/app/Http/Controllers/Api/Service/OrderController.php b/app/Http/Controllers/Api/Service/OrderController.php index 4942d15..9f2cda9 100644 --- a/app/Http/Controllers/Api/Service/OrderController.php +++ b/app/Http/Controllers/Api/Service/OrderController.php @@ -9,6 +9,7 @@ use App\Repositories\Api\Order\OrderRepository; use App\Services\Api\Order\OrderService; use App\Enums\Order\OrderStatus; use App\Enums\Invoice\InvoiceStatus; +use App\Models\Invoice as InvoiceModel; use App\Models\Order as OrderModel; use Carbon\Carbon; use Illuminate\Http\JsonResponse; @@ -279,6 +280,8 @@ class OrderController extends Controller 'amount' => (float) $order->amount, ], 'service' => [ + 'product_id' => $firstItem?->product?->id, + 'product_uuid' => $firstItem?->product?->uuid, 'name' => $firstItem?->product?->name, 'billing_cycle' => $firstItem?->billing_cycle, 'server_name' => $firstItem?->server_name, @@ -338,9 +341,11 @@ class OrderController extends Controller 'id' => $invoice->id, 'invoice_num' => $invoice->order_num, 'service_name' => $firstItem?->product?->name ?? ('Đơn hàng ' . $order?->order_num), + 'product_type' => $firstItem?->product?->product_type, 'server_name' => $firstItem?->server_name, 'billing_cycle'=> $firstItem?->billing_cycle, 'amount' => (float) ($invoice->total ?? 0), + 'auto_renew' => (bool) ($invoice->auto_renew ?? false), 'vm_id' => $invoice->vm_id, 'ip_address' => $vmware?->ipAddress?->ip, 'os_name' => $vmware?->os?->name, @@ -365,6 +370,135 @@ class OrderController extends Controller } } + public function getClientInvoiceDetailByOrderNum(Request $request, string $orderNum): JsonResponse + { + try { + $customer = $request->user(); + $order = OrderModel::query() + ->with(['invoice', 'orderItems.product', 'customer', 'vmWare.ipAddress', 'vmWare.os']) + ->where('customer_id', $customer->id) + ->where('order_num', $orderNum) + ->first(); + + if (!$order || !$order->invoice) { + return response()->json([ + 'success' => false, + 'message' => 'Hóa đơn không tồn tại', + ], 404); + } + + $invoice = $order->invoice; + $firstItem = $order->orderItems->first(); + $vmware = $order->vmWare->first(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $invoice->id, + 'invoice_num' => $invoice->invoice_num, + 'status' => $invoice->status, + 'status_text' => InvoiceStatus::from((int) $invoice->status)->label(), + 'sub_total' => (float) ($invoice->sub_total ?? 0), + 'tax' => (float) ($invoice->tax ?? 0), + 'total' => (float) ($invoice->total ?? 0), + 'created_at' => $this->toIsoDate($invoice->created_at), + 'due_date' => $this->toIsoDate($invoice->due_date), + 'paid_at' => $this->toIsoDate($invoice->date_paid), + 'payment_method' => $order->payment_method ?? null, + 'order' => [ + 'id' => $order->id, + 'order_num' => $order->order_num, + 'amount' => (float) $order->amount, + ], + 'service' => [ + 'product_id' => $firstItem?->product?->id, + 'product_uuid' => $firstItem?->product?->uuid, + 'name' => $firstItem?->product?->name, + 'billing_cycle' => $firstItem?->billing_cycle, + 'server_name' => $firstItem?->server_name, + 'product_type' => $firstItem?->product?->product_type, + 'specs' => [ + 'cpu' => $firstItem?->product?->cpu, + 'ram' => $firstItem?->product?->ram, + 'disk' => $firstItem?->product?->disk, + ], + ], + 'customer' => [ + 'id' => $order->customer?->id, + 'name' => $order->customer?->fullname, + 'email' => $order->customer?->email, + 'address' => $order->customer?->address, + ], + 'items' => $order->orderItems->map(function ($item) { + return [ + 'id' => $item->id, + 'product_name' => $item->product?->name, + 'billing_cycle' => $item->billing_cycle, + 'server_name' => $item->server_name, + 'price' => (float) $item->price, + ]; + }), + 'vmware' => $vmware ? [ + 'id' => $vmware->id, + 'name' => $vmware->name, + 'hostname' => $vmware->hostname, + 'ip_address' => $vmware->ipAddress?->ip, + 'os_name' => $vmware->os?->name, + 'status' => $vmware->status, + 'username' => $vmware->user, + 'port' => $vmware->port, + 'next_due_date' => $this->toIsoDate($vmware->next_due_date), + ] : null, + ], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function updateClientServiceAutoRenew(Request $request, int $id): JsonResponse + { + try { + $customer = $request->user(); + $validated = $request->validate([ + 'auto_renew' => ['required', 'boolean'], + ]); + + $invoice = InvoiceModel::query() + ->where('id', $id) + ->where('customer_id', $customer->id) + ->first(); + + if (!$invoice) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy dịch vụ tương ứng', + ], 404); + } + + $invoice->auto_renew = (bool) $validated['auto_renew']; + $invoice->save(); + + return response()->json([ + 'success' => true, + 'message' => 'Cập nhật tự động gia hạn thành công', + 'data' => [ + 'id' => $invoice->id, + 'invoice_num' => $invoice->order_num, + 'auto_renew' => (bool) $invoice->auto_renew, + ], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + private function toIsoDate($value): ?string { if (empty($value)) { diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index d258e2c..103d5b0 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -1522,6 +1522,62 @@ PS; return "Login_Again"; } + public function restartVM($product, $vmware_id = ''): array|string + { + try { + $vmId = $vmware_id ?: ($product->vmware_id ?? null); + if (!$vmId) { + return [ + 'error' => true, + 'data' => 'Không tìm thấy vmware_id', + ]; + } + + // Ưu tiên quan hệ trực tiếp trên ModuleVmware + $vcenter = $product->vCenter ?? null; + + // Fallback cho dữ liệu cũ chưa có vcenter_id + if (!$vcenter && isset($product->ipAddress) && $product->ipAddress?->host?->datacenter?->vCenter) { + $vcenter = $product->ipAddress->host->datacenter->vCenter; + } + + if (!$vcenter) { + return [ + 'error' => true, + 'data' => 'Không tìm thấy thông tin vCenter cho VM', + ]; + } + + $check = $this->loginServer($vcenter); + if (!$check) { + return "Login_Again"; + } + + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->token + ]) + ->post("https://$this->apiUri/rest/vcenter/vm/$vmId/guest/power?action=reboot"); + + if ($response->status() === 200) { + return [ + 'error' => false, + 'data' => 'success' + ]; + } + + return [ + 'error' => true, + 'data' => $response->json('value') ?? $response->body(), + ]; + } catch (\Throwable $e) { + return [ + 'error' => true, + 'data' => $e->getMessage(), + ]; + } + } + public function StartVirtualMachine($product, $vmware_id = ''): array|string { @@ -1617,7 +1673,7 @@ PS; return "Login_Again"; } - public function CreateSnapShot($product): string|array + public function CreateSnapShot($product, ?string $snapshotName = null): string|array { try { $this->username = $product->ipaddress->host->datacenter->vCenter->user; @@ -1632,7 +1688,7 @@ PS; if ($check) { try { - $Snapshot = $vmware_name . "_" . date('d_m_Y_H_i_s'); + $Snapshot = $snapshotName ?: ($vmware_name . "_" . date('d_m_Y_H_i_s')); Log::channel('Custom')->info("==================================================================="); diff --git a/app/Models/ModuleSnapshot.php b/app/Models/ModuleSnapshot.php index bb59040..d588c0d 100644 --- a/app/Models/ModuleSnapshot.php +++ b/app/Models/ModuleSnapshot.php @@ -20,6 +20,9 @@ class ModuleSnapshot extends Model 'snapshot_ref', 'vcenter_task_id', 'error_message', + 'size_bytes', + 'size_gb', + 'size_last_synced_at', ]; public function vm() diff --git a/database/migrations/2026_03_02_100000_add_auto_renew_to_invoice_table.php b/database/migrations/2026_03_02_100000_add_auto_renew_to_invoice_table.php new file mode 100644 index 0000000..1e24639 --- /dev/null +++ b/database/migrations/2026_03_02_100000_add_auto_renew_to_invoice_table.php @@ -0,0 +1,25 @@ +boolean('auto_renew')->default(false)->after('status'); + }); + } + } + + public function down(): void + { + if (Schema::hasColumn('invoice', 'auto_renew')) { + Schema::table('invoice', function (Blueprint $table) { + $table->dropColumn('auto_renew'); + }); + } + } +}; diff --git a/resources/views/administrator/layout/master2.blade.php b/resources/views/administrator/layout/master2.blade.php new file mode 100644 index 0000000..3fff6d8 --- /dev/null +++ b/resources/views/administrator/layout/master2.blade.php @@ -0,0 +1,96 @@ + + + + + + + + + + Administrator + + + @vite(['resources/js/pages/color-modes.js']) + + + + + + + + + + + + + + + + + + + + @stack('plugin-styles') + + + @vite(['resources/sass/app.scss', 'resources/css/custom.css']) + + + + + @stack('style') + + + + + + +
+
+
+
+ @yield('content') +
+
+
+ + + @vite(['resources/js/app-vite.js']) + + + + + @stack('plugin-scripts') + + + + @vite(['resources/js/pages/template.js']) + + + @include('administrator.layout.partials.toast') + + @stack('custom-scripts') + + + \ No newline at end of file diff --git a/resources/views/administrator/pages/auth/login.blade.php b/resources/views/administrator/pages/auth/login.blade.php index 751573b..890615f 100644 --- a/resources/views/administrator/pages/auth/login.blade.php +++ b/resources/views/administrator/pages/auth/login.blade.php @@ -1,4 +1,4 @@ -@extends('administrator.layout.master') +@extends('administrator.layout.master2') @section('content')
@@ -7,9 +7,9 @@
-
-
-
+
+
+
{{ setting('setting.company') }}
@@ -17,22 +17,22 @@
-
-
+
+
- -
+ +
-
-
-
-
- - + + + + + + @endsection @push('plugin-scripts') @endpush diff --git a/routes/client.php b/routes/client.php index 179bb73..43640a3 100644 --- a/routes/client.php +++ b/routes/client.php @@ -15,6 +15,7 @@ Route::prefix('v1')->as('api.')->group(function () { Route::controller(ClientController::class)->group(function () { Route::post('login', 'login')->name('login'); Route::post('register', 'register')->name('register'); + Route::post('refresh', 'refresh')->name('refresh'); Route::group(['middleware' => ['api.auth']], function () { Route::get('me', 'me')->name('me'); @@ -28,8 +29,66 @@ Route::prefix('v1')->as('api.')->group(function () { Route::get('orders/{id}', 'getClientOrderDetail')->name('orders.detail'); Route::get('invoices', 'getClientInvoices')->name('invoices.list'); Route::get('invoices/{id}', 'getClientInvoiceDetail')->name('invoices.detail'); + Route::get('invoices/order/{orderNum}', 'getClientInvoiceDetailByOrderNum')->name('invoices.detailByOrderNum'); Route::get('dich-vu', 'getClientServices')->name('services.list'); + Route::patch('dich-vu/{id}/auto-renew', 'updateClientServiceAutoRenew')->name('services.autoRenew.update'); }); + + Route::middleware(['api.auth'])->get('get-ticket-console', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $orderNum = (string)$request->query('order_num', ''); + if ($orderNum === '') { + return response()->json(['error' => true, 'message' => 'Thiếu order_num'], 422); + } + + $customer = $request->user(); + $order = \App\Models\Order::with(['vmWare.vCenter']) + ->where('customer_id', $customer->id) + ->where('order_num', $orderNum) + ->first(); + + if (!$order) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy đơn hàng'], 404); + } + + $vm = $order->vmWare->first(); + if (!$vm) { + return response()->json(['error' => true, 'message' => 'Đơn hàng chưa có VM'], 404); + } + + $vcenter = $vm->vCenter; + if (!$vcenter) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy thông tin vCenter'], 404); + } + + if (!$serviceVCenter->loginServer($vcenter)) { + return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vào vCenter']); + } + + $consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id); + + if ($consoleTicket === 'Login_Again') { + if ($serviceVCenter->loginServer($vcenter)) { + $consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id); + } else { + return response()->json(['error' => true, 'message' => 'Không thể đăng nhập lại vào vCenter']); + } + } + + if (is_string($consoleTicket)) { + return response()->json(['error' => true, 'message' => "Failed to get console ticket: $consoleTicket"]); + } + + if ($consoleTicket['error']) { + return response()->json(['error' => true, 'message' => $consoleTicket['data']]); + } + + return response()->json([ + 'error' => false, + 'data' => $consoleTicket['data'], + 'order_num' => $order->order_num, + 'vm_id' => $vm->id, + ]); + })->name('client.console-ticket'); }); // Services (Products) - Public & Protected Routes @@ -126,6 +185,434 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { 'data' => $ConsoleTicket['data'], // wss://10.10.10.120:443/ticket/... ]); }); + + Route::post('vm-restart', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $productId = $request->input('product'); + $customer = $request->user(); + + if (!$productId) { + return response()->json([ + 'error' => true, + 'message' => 'Thiếu product id', + ], 422); + } + + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($productId); + if (!$product) { + return response()->json([ + 'error' => true, + 'message' => 'Không tìm thấy VM', + ], 404); + } + + if ((int)$product->customer_id !== (int)$customer->id) { + return response()->json([ + 'error' => true, + 'message' => 'Bạn không có quyền thao tác VM này', + ], 403); + } + + $result = $serviceVCenter->restartVM($product, $product->vmware_id); + + if (is_string($result) || ($result['error'] ?? true)) { + return response()->json([ + 'error' => true, + 'message' => is_string($result) ? $result : ($result['data'] ?? 'Không thể restart VM'), + ], 400); + } + + return response()->json([ + 'error' => false, + 'message' => 'Đã gửi lệnh restart VM', + 'data' => $result['data'] ?? 'success', + ]); + }); + + Route::post('vm-create-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $customer = $request->user(); + $payload = $request->validate([ + 'product' => ['required', 'integer'], + 'name' => ['nullable', 'string', 'max:191'], + 'description' => ['nullable', 'string', 'max:1000'], + ]); + + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']); + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$product->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $result = $serviceVCenter->CreateSnapShot($product, $payload['name'] ?? null); + if (is_string($result) || ($result['error'] ?? true)) { + return response()->json([ + 'error' => true, + 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể tạo snapshot'), + ], 400); + } + + $snapshotName = $result['data'] ?? ($payload['name'] ?? null); + $snapshot = \App\Models\ModuleSnapshot::create([ + 'name' => $snapshotName, + 'description' => $payload['description'] ?? null, + 'customer_id' => $customer->id, + 'module_vmware_id' => $product->id, + 'status' => 'success', + 'snapshot_ref' => $snapshotName, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Đã tạo snapshot thành công', + 'data' => [ + 'id' => $snapshot->id, + 'name' => $snapshot->name, + 'description' => $snapshot->description, + 'snapshot_ref' => $snapshot->snapshot_ref, + ], + ]); + }); + + Route::get('vm-snapshots', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $customer = $request->user(); + $productId = (int)$request->query('product'); + if (!$productId) { + return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); + } + + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($productId); + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$product->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $remote = $serviceVCenter->getSnapShot($product); + if (is_string($remote) || ($remote['error'] ?? true)) { + return response()->json([ + 'error' => true, + 'message' => is_string($remote) ? $remote : ($remote['message'] ?? 'Không thể lấy danh sách snapshot'), + ], 400); + } + + $remoteItems = $remote['data'] ?? []; + foreach ($remoteItems as $item) { + $ref = (string)($item['id'] ?? $item['name'] ?? ''); + $name = (string)($item['name'] ?? $ref); + if ($name === '') { + continue; + } + + $snapshot = \App\Models\ModuleSnapshot::query() + ->where('module_vmware_id', $product->id) + ->where(function ($q) use ($ref, $name) { + $q->where('snapshot_ref', $ref)->orWhere('name', $name); + }) + ->first(); + + $payload = [ + 'name' => $name, + 'description' => $item['description'] ?? null, + 'customer_id' => $customer->id, + 'module_vmware_id' => $product->id, + 'status' => 'success', + 'snapshot_ref' => $ref ?: $name, + 'size_gb' => isset($item['size_gb']) ? (float)$item['size_gb'] : null, + 'size_last_synced_at' => now(), + ]; + + if ($snapshot) { + $snapshot->update($payload); + } else { + \App\Models\ModuleSnapshot::create($payload); + } + } + + $dbSnapshots = \App\Models\ModuleSnapshot::query() + ->where('module_vmware_id', $product->id) + ->where('customer_id', $customer->id) + ->orderByDesc('created_at') + ->get() + ->map(function ($snap) { + return [ + 'id' => $snap->id, + 'name' => $snap->name, + 'description' => $snap->description, + 'snapshot_ref' => $snap->snapshot_ref, + 'status' => $snap->status, + 'size_gb' => $snap->size_gb, + 'created_at' => $snap->created_at?->toISOString(), + ]; + }); + + return response()->json([ + 'error' => false, + 'data' => $dbSnapshots, + ]); + }); + + Route::post('vm-revert-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $customer = $request->user(); + $payload = $request->validate([ + 'product' => ['required', 'integer'], + 'snapshot' => ['required', 'string'], + ]); + + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']); + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$product->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $result = $serviceVCenter->RevertSnapShot($product, $payload['snapshot']); + if (is_string($result) || ($result['error'] ?? true)) { + return response()->json([ + 'error' => true, + 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể revert snapshot'), + ], 400); + } + + return response()->json([ + 'error' => false, + 'message' => 'Đã gửi lệnh revert snapshot', + ]); + }); + + Route::post('vm-delete-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $customer = $request->user(); + $payload = $request->validate([ + 'product' => ['required', 'integer'], + 'snapshot' => ['required', 'string'], + ]); + + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']); + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$product->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $result = $serviceVCenter->RemoveSnapShot($product, $payload['snapshot']); + if (is_string($result) || ($result['error'] ?? true)) { + return response()->json([ + 'error' => true, + 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể xoá snapshot'), + ], 400); + } + + \App\Models\ModuleSnapshot::query() + ->where('module_vmware_id', $product->id) + ->where('customer_id', $customer->id) + ->where(function ($q) use ($payload) { + $q->where('snapshot_ref', $payload['snapshot'])->orWhere('name', $payload['snapshot']); + }) + ->delete(); + + return response()->json([ + 'error' => false, + 'message' => 'Đã xoá snapshot thành công', + ]); + }); + + Route::get('vm-rebuild-options', function (\Illuminate\Http\Request $request) { + $customer = $request->user(); + $productId = (int)$request->query('product'); + if (!$productId) { + return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); + } + + $vm = \App\Models\ModuleVmware::with(['order.orderItems.product.osVersions.osGroup']) + ->find($productId); + if (!$vm) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$vm->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $product = optional($vm->order?->orderItems?->first())->product; + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); + } + + $osOptions = $product->osVersions + ->where('status', 1) + ->map(fn($os) => [ + 'id' => $os->id, + 'name' => $os->name, + 'version' => $os->version, + 'group' => $os->osGroup?->name, + ]) + ->values(); + + return response()->json([ + 'error' => false, + 'data' => [ + 'product_id' => $product->id, + 'product_uuid' => $product->uuid, + 'product_name' => $product->name, + 'os_options' => $osOptions, + ], + ]); + }); + + Route::post('vm-rebuild', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $customer = $request->user(); + $payload = $request->validate([ + 'product' => ['required', 'integer'], + 'os_id' => ['required', 'integer', 'exists:os,id'], + ]); + + $vm = \App\Models\ModuleVmware::with(['order.orderItems.product', 'vCenter', 'ipAddress.host.datacenter.vCenter']) + ->find($payload['product']); + if (!$vm) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$vm->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $product = optional($vm->order?->orderItems?->first())->product; + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); + } + + $allowedOs = $product->osVersions()->where('status', 1)->pluck('id')->toArray(); + if (!in_array((int)$payload['os_id'], array_map('intval', $allowedOs), true)) { + return response()->json(['error' => true, 'message' => 'OS không hợp lệ cho product hiện tại'], 422); + } + + \Illuminate\Support\Facades\DB::table('reinstall_logs')->insert([ + 'product_id' => $vm->id, + 'product_type' => 4, + 'custome_id' => $customer->id, + 'os_id' => $payload['os_id'], + 'status' => 'pending', + 'initiator' => 'client', + 'note' => 'Client request rebuild from portal-client', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $result = $serviceVCenter->reBuildVPS((string)$vm->name); + if (is_string($result) || ($result['error'] ?? false)) { + return response()->json([ + 'error' => true, + 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể rebuild VM'), + ], 400); + } + + return response()->json([ + 'error' => false, + 'message' => 'Đã gửi yêu cầu rebuild VM', + 'data' => $result, + ]); + }); + + Route::get('vm-addon-options', function (\Illuminate\Http\Request $request) { + $customer = $request->user(); + $productId = (int)$request->query('product'); + if (!$productId) { + return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); + } + + $vm = \App\Models\ModuleVmware::with(['order.orderItems.product.addons']) + ->find($productId); + if (!$vm) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$vm->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $product = optional($vm->order?->orderItems?->first())->product; + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); + } + + $addons = $product->addons()->where('status', true)->get()->map(function ($addon) { + return [ + 'id' => $addon->id, + 'code' => strtolower((string)$addon->code), + 'name' => $addon->name, + 'price' => (float)$addon->price, + 'min' => (int)($addon->min ?? 0), + 'max' => (int)($addon->max ?? 0), + 'step' => (int)($addon->step ?? 1), + ]; + }); + + return response()->json([ + 'error' => false, + 'data' => [ + 'product_id' => $product->id, + 'product_name' => $product->name, + 'base_specs' => [ + 'cpu' => (int)($product->cpu ?? 0), + 'ram' => (int)($product->ram ?? 0), + 'disk' => (int)($product->disk ?? 0), + 'bandwidth' => (int)($product->bandwidth ?? 0), + 'gpu' => $product->gpu, + ], + 'addons' => $addons->values(), + ], + ]); + }); + + Route::post('vm-upgrade-addons', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + $customer = $request->user(); + $payload = $request->validate([ + 'product' => ['required', 'integer'], + 'cpu' => ['nullable', 'integer', 'min:0'], + 'ram' => ['nullable', 'integer', 'min:0'], + 'disk' => ['nullable', 'integer', 'min:0'], + 'bandwidth' => ['nullable', 'integer', 'min:0'], + 'gpu' => ['nullable', 'string', 'max:255'], + ]); + + $vm = \App\Models\ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter']) + ->find($payload['product']); + if (!$vm) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + if ((int)$vm->customer_id !== (int)$customer->id) { + return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + } + + $vcenter = $vm->vCenter ?? $vm->ipAddress?->host?->datacenter?->vCenter; + if (!$vcenter || !$serviceVCenter->loginServer($vcenter)) { + return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vCenter'], 400); + } + + $applied = []; + + if (isset($payload['cpu']) && $payload['cpu'] > 0) { + $res = $serviceVCenter->UpgradeCPUVirtualMachine($vm->vmware_id, (int)$payload['cpu']); + if (is_array($res) && !($res['error'] ?? true)) $applied[] = 'cpu'; + } + if (isset($payload['ram']) && $payload['ram'] > 0) { + $res = $serviceVCenter->UpgradeMemoryVirtualMachine($vm->vmware_id, (int)$payload['ram']); + if (is_array($res) && !($res['error'] ?? true)) $applied[] = 'ram'; + } + if (isset($payload['disk']) && $payload['disk'] > 0) { + $res = $serviceVCenter->UpgradeDiskVirtualMachine($vm, (int)$payload['disk']); + if (is_array($res) && !($res['error'] ?? true)) $applied[] = 'disk'; + } + + return response()->json([ + 'error' => false, + 'message' => 'Đã gửi yêu cầu nâng cấp addons', + 'data' => [ + 'applied' => $applied, + 'note' => 'bandwidth/gpu hiện được ghi nhận yêu cầu, chưa có luồng tự động áp vào VM', + ], + ]); + }); }); diff --git a/test-console-ticket.php b/test-console-ticket.php deleted file mode 100644 index 77a2ec8..0000000 --- a/test-console-ticket.php +++ /dev/null @@ -1,25 +0,0 @@ -make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - -$vCenter = app(\App\Http\Services\vCenter::class); -$vm = \App\Models\VirtualMachines::with('datacenter.vCenter')->first(); - -if (!$vm) { - echo "No VM found\n"; - exit; -} - -$vcenter_model = $vm->datacenter->vCenter; -echo "Logging in to vCenter: " . $vcenter_model->host . "\n"; -$loggedIn = $vCenter->loginServer($vcenter_model); - -if (!$loggedIn) { - echo "Failed to login to vCenter\n"; - exit; -} -echo "Logged in successfully\n"; - -$ticket = $vCenter->GetConsoleTickets($vm->vm_id); -print_r($ticket);