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')
+
+
+
+
+
+
+
+
+
+ @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')
-
-
+
+
+
+
+
+
@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);