update optimize code

This commit is contained in:
pbao9
2025-07-29 15:48:51 +07:00
parent e8656c5655
commit d80580d1e4
24 changed files with 997 additions and 18755 deletions
-246
View File
@@ -1,246 +0,0 @@
# Tóm tắt thay đổi - Xử lý Addon sau khi Clone VM
## Tổng quan
Tài liệu này mô tả các thay đổi đã thực hiện để cải thiện hệ thống quản lý vCenter virtual machines trong Laravel application.
## Các thay đổi chính
### 1. Cải thiện vCenter Service (app/Http/Services/vCenter.php)
#### 1.1 Thêm các method cập nhật phần cứng VM
- **updateVMHardware()**: Method chính để cập nhật cấu hình phần cứng VM
- **updateVMCPU()**: Cập nhật số lượng CPU cores
- **updateVMMemory()**: Cập nhật RAM (convert GB sang MiB)
- **updateVMDisk()**: Cập nhật dung lượng disk (mở rộng disk)
#### 1.2 Cải thiện CreateVirtualMachine()
- Thêm logic để áp dụng addon sau khi clone VM thành công
- Convert RAM từ GB sang MiB trước khi gọi API
- Thêm logging chi tiết cho quá trình tạo VM và cập nhật addon
- Gửi thông báo Discord khi tạo VM thành công
#### 1.3 Thêm method test
- **testAddonUpdate()**: Method để test việc cập nhật addon
### 2. Cập nhật CreateVirtualMachineJob (app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php)
#### 2.1 Cải thiện xử lý addon
- Parse đúng số lượng addon từ order items
- Truyền thông tin addon vào vCenter service
- Thêm logging chi tiết cho quá trình xử lý
#### 2.2 Lưu vm_id vào invoice
- Lưu VM ID vào bảng `invoice` sau khi tạo thành công
- Cập nhật trạng thái order và gửi email thông báo
### 3. Tạo ServiceController (app/Http/Controllers/frontend/ServiceController.php)
#### 3.1 Chức năng chính
- **show()**: Hiển thị chi tiết service dựa trên invoice_id
- **toggleVM()**: Điều khiển VM (start/stop/restart) - placeholder
- **getVMInfo()**: Lấy thông tin VM từ vCenter - placeholder
#### 3.2 Tích hợp vCenter API
- **getVMInfoFromVCenter()**: Lấy thông tin VM thực từ vCenter API
- **getDefaultVMInfo()**: Thông tin mặc định khi không thể kết nối vCenter
- Tự động login vào vCenter và lấy thông tin chi tiết VM
#### 3.3 Thông tin VM được lấy
- Trạng thái power (POWERED_ON, POWERED_OFF, SUSPENDED)
- IP address từ guest info
- Thông tin phần cứng (CPU, RAM, Disk)
- Thông tin chi tiết (folder, resource pool, datastore, network, etc.)
### 4. Tạo Service Detail View (resources/views/frontend/service/show.blade.php)
#### 4.1 Giao diện hiển thị
- Thông tin cơ bản của service (invoice, customer, billing)
- Thông tin chi tiết VM từ vCenter API
- Danh sách addon đã mua
- Nút điều khiển VM (placeholder)
#### 4.2 Thông tin VM hiển thị
- VM ID, tên VM, trạng thái
- IP address, hostname, MAC address
- CPU, RAM, Disk, OS
- Thông tin chi tiết: folder, resource pool, datastore, network, version
### 5. Cập nhật Routes (routes/web.php)
#### 5.1 Thêm routes cho service
```php
Route::get('/service/{invoice_id}', [ServiceController::class, 'show'])->name('service.show');
Route::post('/service/{invoice_id}/toggle-vm', [ServiceController::class, 'toggleVM'])->name('service.toggle-vm');
Route::get('/service/{invoice_id}/vm-info', [ServiceController::class, 'getVMInfo'])->name('service.vm-info');
```
### 6. Cập nhật My Service View (resources/views/frontend/my-service.blade.php)
#### 6.1 Thêm link chi tiết
- Cập nhật link "Xem chi tiết" để trỏ đến route mới
- Hiển thị VM ID nếu có
### 7. Cập nhật Admin Invoice Views
#### 7.1 Hiển thị VM ID
- **resources/views/admin/invoice/show.blade.php**: Thêm cột VM ID
- **resources/views/admin/invoice/index.blade.php**: Thêm cột VM ID
### 8. Tạo Test Commands
#### 8.1 TestVMInfo Command (app/Console/Commands/TestVMInfo.php)
- Test việc lấy thông tin VM từ vCenter API
- Kiểm tra login, VM details, power status, IP info
- Hiển thị thông tin chi tiết để debug
#### 8.2 TestSaveVMId Command (app/Console/Commands/TestSaveVMId.php)
- Test việc lưu VM ID vào invoice
- Simulate quá trình tạo VM và lưu thông tin
#### 8.3 TestCreateVMWithAddon Command (app/Console/Commands/TestCreateVMWithAddon.php)
- Test toàn bộ quá trình tạo VM với addon
- Từ order đến tạo VM và cập nhật addon
## Các vấn đề đã giải quyết
### 1. Vấn đề addon không áp dụng
- **Nguyên nhân**: Logic cũ không cập nhật phần cứng sau khi clone VM
- **Giải pháp**: Thêm logic cập nhật phần cứng sau khi clone thành công
### 2. Vấn đề convert RAM
- **Nguyên nhân**: RAM được truyền dưới dạng GB nhưng API yêu cầu MiB
- **Giải pháp**: Convert GB sang MiB (1 GB = 1024 MiB) trước khi gọi API
### 3. Vấn đề hiển thị thông tin VM
- **Nguyên nhân**: Không có cách để xem thông tin VM thực từ vCenter
- **Giải pháp**: Tạo service detail page tích hợp với vCenter API
### 4. Vấn đề lưu VM ID
- **Nguyên nhân**: VM ID không được lưu vào database
- **Giải pháp**: Lưu VM ID vào bảng `invoice` sau khi tạo thành công
## Cách sử dụng
### 1. Test lấy thông tin VM
```bash
php artisan test:vm-info {invoice_id}
```
### 2. Test lưu VM ID
```bash
php artisan test:save-vm-id {invoice_id}
```
### 3. Test tạo VM với addon
```bash
php artisan test:create-vm-with-addon {order_id}
```
### 4. Truy cập service detail
- URL: `/service/{invoice_id}`
- Yêu cầu đăng nhập và quyền sở hữu invoice
## Lưu ý quan trọng
### 1. Bảo mật
- Kiểm tra quyền truy cập: chỉ customer sở hữu mới được xem service detail
- Validate input và handle exception cẩn thận
### 2. Performance
- Cache thông tin VM nếu cần thiết
- Timeout cho vCenter API calls
- Fallback khi không thể kết nối vCenter
### 3. Monitoring
- Log chi tiết tất cả operations
- Gửi thông báo Discord cho các events quan trọng
- Track performance của vCenter API calls
## Các bước tiếp theo
### 1. Implement VM Control Actions
- Start/Stop/Restart VM thực sự
- Console access
- Snapshot management
### 2. Cải thiện UI/UX
- Real-time status updates
- Progress indicators
- Better error handling
### 3. Monitoring & Alerting
- VM health monitoring
- Resource usage tracking
- Automated alerts
### 4. Backup & Recovery
- Automated backup scheduling
- Disaster recovery procedures
- Backup verification
## Database Changes
### 1. Invoice Table
- Thêm cột `vm_id` để lưu VM ID từ vCenter
- Cột này được populate sau khi tạo VM thành công
### 2. Không có migration cần thiết
- Cột `vm_id` đã tồn tại trong database
## API Endpoints
### 1. vCenter API Integration
- `GET /rest/vcenter/vm/{vm_id}` - Lấy thông tin VM
- `GET /rest/vcenter/vm/{vm_id}/guest/power` - Lấy trạng thái power
- `GET /rest/vcenter/vm/{vm_id}/guest/identity` - Lấy thông tin guest
- `PATCH /rest/vcenter/vm/{vm_id}/hardware/cpu` - Cập nhật CPU
- `PATCH /rest/vcenter/vm/{vm_id}/hardware/memory` - Cập nhật RAM
### 2. Application API
- `GET /service/{invoice_id}` - Service detail page
- `POST /service/{invoice_id}/toggle-vm` - Điều khiển VM
- `GET /service/{invoice_id}/vm-info` - Lấy thông tin VM
## Testing
### 1. Unit Tests
- Test vCenter service methods
- Test VM creation workflow
- Test addon application logic
### 2. Integration Tests
- Test vCenter API integration
- Test complete VM creation flow
- Test service detail page
### 3. Manual Testing
- Test với VM thực trên vCenter
- Test các trường hợp lỗi
- Test performance với nhiều VM
## Troubleshooting
### 1. VM Creation Issues
- Kiểm tra vCenter connectivity
- Verify template availability
- Check resource availability
### 2. Addon Application Issues
- Verify addon quantities
- Check API responses
- Review logs for errors
### 3. Service Detail Page Issues
- Check vCenter login
- Verify VM ID exists
- Review API responses
## Conclusion
Các thay đổi này đã cải thiện đáng kể hệ thống quản lý VM:
- Tự động áp dụng addon sau khi tạo VM
- Hiển thị thông tin VM thực từ vCenter
- Giao diện quản lý service chi tiết
- Monitoring và logging tốt hơn
Hệ thống hiện tại đã sẵn sàng cho production với các tính năng cơ bản hoạt động ổn định.
@@ -48,8 +48,6 @@ class ServiceController extends Controller
return redirect()->route('service.index')->with('error', 'Bạn không có quyền truy cập dịch vụ này.');
}
// Lấy thông tin VM nếu có - WITH CACHING AND BACKGROUND UPDATE
$vmInfo = null;
if ($invoice->vm_id) {
@@ -234,10 +232,10 @@ class ServiceController extends Controller
/**
* Action để bật/tắt VM (placeholder - chưa implement)
*/
public function toggleVM(Request $request, $invoice_id)
public function toggleVM(Request $request, $invoice_ordernum)
{
try {
$invoice = Invoice::findOrFail($invoice_id);
$invoice = Invoice::where('order_num', $invoice_ordernum)->first();
// Kiểm tra quyền truy cập
if ($invoice->customer_id !== customer()->id) {
@@ -261,7 +259,7 @@ class ServiceController extends Controller
return response()->json($response);
} catch (\Exception $e) {
// Log::error('Error in ServiceController@toggleVM: ' . $e->getMessage());
return response()->json(['error' => true, 'message' => 'Có lỗi xảy ra khi thực hiện thao tác.']);
return response()->json(['error' => true, 'message' => 'Có lỗi xảy ra khi thực hiện thao tác. ' . $e->getMessage()]);
}
}
@@ -433,199 +431,110 @@ class ServiceController extends Controller
/**
* Secure get snapshots using session authentication - NO BEARER TOKEN
*/
/**
* Lấy danh sách snapshot với bảo mật và tối ưu hóa
*/
public function getSnapshots(Request $request, vCenter $serviceVCenter)
{
try {
// Kiểm tra authentication
$user = customer();
if (!$user) {
return response()->json([
'error' => true,
'message' => 'Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.',
'debug' => 'User not authenticated via session'
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')
], 401);
}
// Validation với sanitization
$validator = Validator::make($request->all(), [
'product_id' => 'required|string',
'product_id' => 'required|string|max:255',
]);
if ($validator->fails()) {
return response()->json([
'error' => true,
'message' => 'Dữ liệu không hợp lệ',
'message' => __('Dữ liệu không hợp lệ'),
'errors' => $validator->errors()
], 422);
}
$productId = $request->product_id;
$productId = $this->sanitizeInput($request->product_id);
$userId = $user->id;
Log::info('🔍 Debug - Tìm kiếm VPS để lấy snapshot', [
'product_id' => $productId,
'user_id' => $userId
]);
// Cache key cho snapshot data
$cacheKey = "snapshots_{$userId}_{$productId}";
$cacheDuration = 300; // 5 phút
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')
->where('vmware_id', $productId)
->where('customer_id', $userId)
->first();
// Thử lấy từ cache trước
if (Cache::has($cacheKey)) {
$cachedData = Cache::get($cacheKey);
if ($cachedData && !$this->isCacheExpired($cachedData)) {
return response()->json([
'error' => false,
'message' => __('Lấy danh sách snapshot thành công'),
'data' => $cachedData,
'cached' => true
]);
}
}
// Tìm VPS với eager loading tối ưu
$product = $this->findVmwareProduct($productId, $userId);
if (!$product) {
return response()->json([
'error' => true,
'message' => "Không tìm thấy VPS #{$productId} hoặc bạn không có quyền truy cập",
'debug' => 'product not found or unauthorized'
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')
], 404);
}
if (!$product->ipAddress) {
// Kiểm tra cấu hình vCenter
$vcenter = $this->validateVcenterConfiguration($product);
if (!$vcenter) {
return response()->json([
'error' => true,
'message' => "VPS #{$productId} không có thông tin IP được cấu hình",
'debug' => 'product->ipAddress is null'
'message' => __('Cấu hình vCenter không hợp lệ')
], 400);
}
if (!$product->ipAddress->host) {
// Lấy snapshot từ vCenter
$snapshots = $this->fetchSnapshotsFromVCenter($serviceVCenter, $product, $vcenter);
if ($snapshots === false) {
return response()->json([
'error' => true,
'message' => "VPS #{$productId} không có thông tin Host được cấu hình",
'debug' => 'product->ipAddress->host is null'
], 400);
}
if (!$product->ipAddress->host->datacenter) {
return response()->json([
'error' => true,
'message' => "VPS #{$productId} không có thông tin Datacenter được cấu hình",
'debug' => 'product->ipAddress->host->datacenter is null'
], 400);
}
if (!$product->ipAddress->host->datacenter->vCenter) {
return response()->json([
'error' => true,
'message' => "VPS #{$productId} không có thông tin vCenter được cấu hình",
'debug' => 'product->ipAddress->host->datacenter->vCenter is null'
], 400);
}
// Get vCenter configuration
$vcenter = $product->ipAddress->host->datacenter->vCenter;
// Login to vCenter
if (!$serviceVCenter->loginServer($vcenter)) {
return response()->json([
'error' => true,
'message' => 'Không thể đăng nhập vào vCenter'
'message' => __('Không thể kết nối đến vCenter')
], 500);
}
Log::info('�� Bắt đầu lấy danh sách snapshot', [
'product_id' => $productId,
'product_name' => $product->name,
'vmware_id' => $product->vmware_id,
'vcenter_host' => $product->ipAddress->host->datacenter->vCenter->host ?? 'null',
'vcenter_user' => $product->ipAddress->host->datacenter->vCenter->user ?? 'null'
]);
// Lấy thông tin bổ sung từ database
$dbSnapshots = $this->getDatabaseSnapshots($userId, $product->id);
// Check if getSnapShot method exists
if (!method_exists($serviceVCenter, 'getSnapShot')) {
Log::error('❌ getSnapShot method không tồn tại trong ServiceVCenter');
return response()->json([
'error' => true,
'message' => 'getSnapShot method không tồn tại'
], 500);
}
// Merge và sanitize data
$processedSnapshots = $this->processSnapshotsData($snapshots, $dbSnapshots);
$result = $serviceVCenter->getSnapShot($product);
Log::info('✅ getSnapShot function đã được gọi');
// Chuẩn bị response data
$responseData = [
'snapshots' => $processedSnapshots,
'total_count' => count($processedSnapshots),
'vm_name' => $this->sanitizeOutput($product->name),
'vm_id' => $product->vmware_id,
'last_updated' => now()->format('Y-m-d H:i:s')
];
Log::info('📸 Kết quả getSnapShot', [
'result' => $result,
'product_id' => $productId
]);
if ($result === "Login_Again") {
if ($serviceVCenter->loginServer($vcenter)) {
$result = $serviceVCenter->getSnapShot($product);
} else {
return response()->json([
'error' => true,
'message' => 'Không thể đăng nhập lại vào vCenter'
], 500);
}
}
if (is_string($result)) {
return response()->json([
'error' => true,
'message' => "Lỗi lấy snapshot: $result"
], 500);
}
if (isset($result['error']) && $result['error']) {
return response()->json([
'error' => true,
'message' => "Lấy snapshot thất bại: " . ($result['message'] ?? 'Unknown error')
], 500);
}
// Get snapshots from database for additional info
$dbSnapshots = ModuleSnapshot::where('customer_id', $user->id)
->where('module_vmware_id', $product->id)
->get()
->keyBy('name');
// Merge vCenter data with database data
$snapshots = [];
if (isset($result['data']) && is_array($result['data'])) {
foreach ($result['data'] as $snapshot) {
$snapshotName = $snapshot['name'] ?? $snapshot['Name'] ?? 'Unknown';
$dbSnapshot = $dbSnapshots->get($snapshotName);
$snapshots[] = [
'name' => $snapshotName,
'description' => $snapshot['description'] ?? $snapshot['Description'] ?? '',
'created' => $snapshot['created'] ?? $snapshot['Created'] ?? '',
'power_state' => $snapshot['power_state'] ?? $snapshot['PowerState'] ?? '',
'size_mb' => $snapshot['size_mb'] ?? $snapshot['SizeMB'] ?? 0,
'size_gb' => $snapshot['size_gb'] ?? $snapshot['SizeGB'] ?? 0,
'vm_name' => $snapshot['vm_name'] ?? $snapshot['VM'] ?? '',
'vm_id' => $snapshot['vm_id'] ?? $snapshot['VMId'] ?? '',
'parent' => $snapshot['parent'] ?? $snapshot['Parent'] ?? '',
'parent_snapshot_id' => $snapshot['parent_snapshot_id'] ?? $snapshot['ParentSnapshotId'] ?? '',
'parent_snapshot' => $snapshot['parent_snapshot'] ?? $snapshot['ParentSnapshot'] ?? '',
'children' => $snapshot['children'] ?? $snapshot['Children'] ?? '',
'is_current' => $snapshot['is_current'] ?? $snapshot['IsCurrent'] ?? false,
'is_replay_supported' => $snapshot['is_replay_supported'] ?? $snapshot['IsReplaySupported'] ?? false,
'snapshot_id' => $snapshot['id'] ?? $snapshot['Id'] ?? '',
'uid' => $snapshot['uid'] ?? $snapshot['Uid'] ?? '',
'in_database' => $dbSnapshot ? true : false,
'db_created_at' => $dbSnapshot ? $dbSnapshot->created_at->format('d/m/Y H:i:s') : null,
'raw_data' => $snapshot
];
}
}
// Cache kết quả
Cache::put($cacheKey, $responseData, $cacheDuration);
return response()->json([
'error' => false,
'message' => 'Lấy danh sách snapshot thành công',
'data' => [
'snapshots' => $snapshots,
'total_count' => count($snapshots),
'vm_name' => $product->name,
'vm_id' => $product->vmware_id,
'cache_info' => [
'cached' => false,
'cache_key' => "snapshots_{$product->id}_{$product->name}"
]
]
'message' => __('Lấy danh sách snapshot thành công'),
'data' => $responseData,
'cached' => false
]);
} catch (\Exception $e) {
Log::error('❌ Lỗi khi lấy danh sách snapshot', [
'product_id' => $request->product_id ?? 'not_provided',
Log::error('Lỗi khi lấy danh sách snapshot', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
@@ -633,73 +542,7 @@ class ServiceController extends Controller
return response()->json([
'error' => true,
'message' => 'Lỗi hệ thống khi lấy danh sách snapshot: ' . $e->getMessage()
], 500);
}
}
/**
* Secure start VM using session authentication - NO BEARER TOKEN
*/
public function startVM(Request $request, vCenter $vCenterService)
{
try {
// Use session authentication - NO Bearer token required
$user = customer(); // This uses session
if (!$user) {
return response()->json([
'error' => true,
'message' => 'Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.'
], 401);
}
// Implement start VM logic here
return response()->json([
'error' => false,
'message' => 'Chức năng start VM sẽ được thêm vào'
]);
} catch (\Exception $e) {
Log::error('❌ Lỗi trong secure startVM', [
'user_id' => $user->id ?? 'guest',
'error' => $e->getMessage()
]);
return response()->json([
'error' => true,
'message' => 'Lỗi hệ thống: ' . $e->getMessage()
], 500);
}
}
/**
* Secure stop VM using session authentication - NO BEARER TOKEN
*/
public function stopVM(Request $request, vCenter $vCenterService)
{
try {
// Use session authentication - NO Bearer token required
$user = customer(); // This uses session
if (!$user) {
return response()->json([
'error' => true,
'message' => 'Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.'
], 401);
}
// Implement stop VM logic here
return response()->json([
'error' => false,
'message' => 'Chức năng stop VM sẽ được thêm vào'
]);
} catch (\Exception $e) {
Log::error('❌ Lỗi trong secure stopVM', [
'user_id' => $user->id ?? 'guest',
'error' => $e->getMessage()
]);
return response()->json([
'error' => true,
'message' => 'Lỗi hệ thống: ' . $e->getMessage()
'message' => __('Có lỗi xảy ra khi lấy danh sách snapshot')
], 500);
}
}
@@ -887,4 +730,345 @@ class ServiceController extends Controller
], 500);
}
}
/**
* Secure start VM using session authentication - NO BEARER TOKEN
*/
public function startVM(Request $request, vCenter $vCenterService)
{
try {
$user = customer();
if (!$user) {
return response()->json([
'error' => true,
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')
], 401);
}
$validator = \Validator::make($request->all(), [
'product_id' => 'required|string|max:255',
]);
if ($validator->fails()) {
return response()->json([
'error' => true,
'message' => __('Dữ liệu không hợp lệ'),
'errors' => $validator->errors()
], 422);
}
$productId = htmlspecialchars(strip_tags(trim($request->product_id)), ENT_QUOTES, 'UTF-8');
$userId = $user->id;
$product = ModuleVmware::where('vmware_id', $productId)
->where('customer_id', $userId)
->first();
if (!$product) {
return response()->json([
'error' => true,
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')
], 404);
}
$result = $vCenterService->StartVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
// Clear VM info cache after starting VM
\Cache::forget('vm_info_' . $product->vmware_id);
return response()->json([
'error' => false,
'message' => __('Yêu cầu khởi động máy ảo đã được gửi thành công.')
]);
} else {
$msg = is_array($result) && isset($result['data']) ? $result['data'] : __('Không thể khởi động máy ảo');
return response()->json([
'error' => true,
'message' => $msg
], 500);
}
} catch (\Exception $e) {
\Log::error('Lỗi khi khởi động máy ảo', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return response()->json([
'error' => true,
'message' => __('Có lỗi xảy ra khi khởi động máy ảo')
], 500);
}
}
/**
* Secure stop VM using session authentication - NO BEARER TOKEN
*/
public function stopVM(Request $request, vCenter $vCenterService)
{
try {
$user = customer();
if (!$user) {
return response()->json([
'error' => true,
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')
], 401);
}
$validator = \Validator::make($request->all(), [
'product_id' => 'required|string|max:255',
]);
if ($validator->fails()) {
return response()->json([
'error' => true,
'message' => __('Dữ liệu không hợp lệ'),
'errors' => $validator->errors()
], 422);
}
$productId = htmlspecialchars(strip_tags(trim($request->product_id)), ENT_QUOTES, 'UTF-8');
$userId = $user->id;
$product = ModuleVmware::where('vmware_id', $productId)
->where('customer_id', $userId)
->first();
if (!$product) {
return response()->json([
'error' => true,
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')
], 404);
}
$result = $vCenterService->ShutdownGuestVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
// Clear VM info cache after stopping VM
\Cache::forget('vm_info_' . $product->vmware_id);
return response()->json([
'error' => false,
'message' => __('Yêu cầu tắt máy ảo đã được gửi thành công.')
]);
} else {
$msg = is_array($result) && isset($result['data']) ? $result['data'] : __('Không thể tắt máy ảo');
return response()->json([
'error' => true,
'message' => $msg
], 500);
}
} catch (\Exception $e) {
\Log::error('Lỗi khi tắt máy ảo', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return response()->json([
'error' => true,
'message' => __('Có lỗi xảy ra khi tắt máy ảo')
], 500);
}
}
/**
* Secure reboot VM using session authentication - NO BEARER TOKEN
*/
public function rebootVM(Request $request, vCenter $vCenterService)
{
try {
$user = customer();
if (!$user) {
return response()->json([
'error' => true,
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')
], 401);
}
$validator = \Validator::make($request->all(), [
'product_id' => 'required|string|max:255',
]);
if ($validator->fails()) {
return response()->json([
'error' => true,
'message' => __('Dữ liệu không hợp lệ'),
'errors' => $validator->errors()
], 422);
}
$productId = htmlspecialchars(strip_tags(trim($request->product_id)), ENT_QUOTES, 'UTF-8');
$userId = $user->id;
$product = ModuleVmware::where('vmware_id', $productId)
->where('customer_id', $userId)
->first();
if (!$product) {
return response()->json([
'error' => true,
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')
], 404);
}
$result = $vCenterService->RebootVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
return response()->json([
'error' => false,
'message' => __('Yêu cầu khởi động lại máy ảo đã được gửi thành công.')
]);
} else {
$msg = is_array($result) && isset($result['data']) ? $result['data'] : __('Không thể khởi động lại máy ảo');
return response()->json([
'error' => true,
'message' => $msg
], 500);
}
} catch (\Exception $e) {
\Log::error('Lỗi khi reboot VM', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return response()->json([
'error' => true,
'message' => __('Có lỗi xảy ra khi khởi động lại máy ảo')
], 500);
}
}
/**
* Sanitize input data
*/
private function sanitizeInput($input): string
{
return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
}
/**
* Sanitize output data
*/
private function sanitizeOutput($output): string
{
return htmlspecialchars($output, ENT_QUOTES, 'UTF-8');
}
/**
* Kiểm tra cache có hết hạn chưa
*/
private function isCacheExpired($cachedData): bool
{
if (!isset($cachedData['last_updated'])) {
return true;
}
$lastUpdated = \Carbon\Carbon::parse($cachedData['last_updated']);
return now()->diffInMinutes($lastUpdated) > 2; // Cache 2 phút
}
/**
* Tìm VPS với eager loading
*/
private function findVmwareProduct($productId, $userId)
{
return ModuleVmware::with(['ipAddress.host.datacenter.vCenter'])
->where('vmware_id', $productId)
->where('customer_id', $userId)
->first();
}
/**
* Validate vCenter configuration
*/
private function validateVcenterConfiguration($product)
{
if (!$product->ipAddress?->host?->datacenter?->vCenter) {
return null;
}
return $product->ipAddress->host->datacenter->vCenter;
}
/**
* Lấy snapshot từ vCenter
*/
private function fetchSnapshotsFromVCenter($serviceVCenter, $product, $vcenter)
{
try {
// Login to vCenter
if (!$serviceVCenter->loginServer($vcenter)) {
return false;
}
// Kiểm tra method tồn tại
if (!method_exists($serviceVCenter, 'getSnapShot')) {
Log::error('getSnapShot method không tồn tại trong ServiceVCenter');
return false;
}
$result = $serviceVCenter->getSnapShot($product);
// Retry logic
if ($result === "Login_Again") {
if ($serviceVCenter->loginServer($vcenter)) {
$result = $serviceVCenter->getSnapShot($product);
} else {
return false;
}
}
// Validate result
if (is_string($result) || (isset($result['error']) && $result['error'])) {
return false;
}
return $result['data'] ?? [];
} catch (\Exception $e) {
Log::error('Lỗi khi lấy snapshot từ vCenter', [
'product_id' => $product->id,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Lấy snapshot từ database
*/
private function getDatabaseSnapshots($userId, $productId)
{
return ModuleSnapshot::where('customer_id', $userId)
->where('module_vmware_id', $productId)
->get()
->keyBy('name');
}
/**
* Xử lý và sanitize snapshot data
*/
private function processSnapshotsData($snapshots, $dbSnapshots): array
{
$processed = [];
if (!is_array($snapshots)) {
return $processed;
}
foreach ($snapshots as $snapshot) {
$snapshotName = $this->sanitizeOutput($snapshot['name'] ?? $snapshot['Name'] ?? 'Unknown');
$dbSnapshot = $dbSnapshots->get($snapshotName);
$processed[] = [
'name' => $snapshotName,
'description' => $this->sanitizeOutput($snapshot['description'] ?? $snapshot['Description'] ?? ''),
'created' => $this->sanitizeOutput($snapshot['created'] ?? $snapshot['Created'] ?? ''),
'power_state' => $this->sanitizeOutput($snapshot['power_state'] ?? $snapshot['PowerState'] ?? ''),
'size_mb' => (int) ($snapshot['size_mb'] ?? $snapshot['SizeMB'] ?? 0),
'size_gb' => (float) ($snapshot['size_gb'] ?? $snapshot['SizeGB'] ?? 0),
'is_current' => (bool) ($snapshot['is_current'] ?? $snapshot['IsCurrent'] ?? false),
'is_replay_supported' => (bool) ($snapshot['is_replay_supported'] ?? $snapshot['IsReplaySupported'] ?? false),
'in_database' => $dbSnapshot ? true : false,
'db_created_at' => $dbSnapshot ? $dbSnapshot->created_at->format('d/m/Y H:i:s') : null
];
}
return $processed;
}
}
+52 -46
View File
@@ -1201,10 +1201,10 @@ class vCenter
return "Login_Again";
}
public function RebootVirtualMachine($vmware_id = ''): array|string
public function RebootVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->checkApiExits();
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
@@ -1225,10 +1225,10 @@ class vCenter
return "Login_Again";
}
public function StartVirtualMachine($vmware_id = ''): array|string
public function StartVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->checkApiExits();
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
@@ -1249,9 +1249,9 @@ class vCenter
return "Login_Again";
}
public function ShutdownGuestVirtualMachine($vmware_id = ''): array|string
public function ShutdownGuestVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->checkApiExits();
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
@@ -1391,7 +1391,7 @@ class vCenter
public function getSnapShot($product): string|array
{
$startTime = microtime(true);
try {
$this->username = $product->ipaddress->host->datacenter->vCenter->user;
$this->password = SignRSA::DecryptFunc($product->ipaddress->host->datacenter->vCenter->password);
@@ -1399,21 +1399,21 @@ class vCenter
} catch (\Exception $e) {
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
}
$vmware_name = (string) $product->name;
// Check cache first
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$cachedResult = Cache::get($cacheKey);
if ($cachedResult !== null) {
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Snapshot cache hit - Execution time: {$executionTime}ms");
return $cachedResult;
}
$check = $this->checkApiExits();
if ($check) {
try {
Log::channel('Custom')->info("===================================================================");
@@ -1440,22 +1440,22 @@ class vCenter
try {
// Clean up the result - remove any PowerShell formatting
$cleanResult = trim($result);
// Parse the text output from the script
$snapshots = $this->parseSnapshotTextOutput($cleanResult);
if (!empty($snapshots)) {
$response = [
'error' => false,
'data' => $snapshots
];
// Cache the result for 5 minutes
Cache::put($cacheKey, $response, now()->addMinutes(5));
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Snapshot retrieved successfully - Total time: {$totalTime}ms, Command time: {$commandTime}ms");
return $response;
} else {
$response = [
@@ -1463,10 +1463,10 @@ class vCenter
'message' => 'Không tìm thấy snapshot nào hoặc lỗi parse dữ liệu',
'raw_result' => $result
];
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 No snapshots found - Total time: {$totalTime}ms");
return $response;
}
} catch (\Exception $parseError) {
@@ -1476,10 +1476,10 @@ class vCenter
'message' => 'Lỗi parse kết quả snapshot: ' . $parseError->getMessage(),
'raw_result' => $result
];
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Parse error - Total time: {$totalTime}ms");
return $response;
}
} else {
@@ -1488,10 +1488,10 @@ class vCenter
'message' => 'Không thể thực thi lệnh hoặc đăng nhập thất bại',
'raw_result' => $result
];
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Command failed - Total time: {$totalTime}ms");
return $response;
}
} catch (\Exception $error) {
@@ -1500,20 +1500,20 @@ class vCenter
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Exception occurred - Total time: {$totalTime}ms");
return [
'error' => true,
'message' => $error->getMessage()
];
}
}
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Login again required - Total time: {$totalTime}ms");
return "Login_Again";
}
@@ -1767,7 +1767,8 @@ class vCenter
// Kiểm tra hệ điều hành
if (str_contains($this->os, "Windows")) {
$user = $data['user'] ?? 'administartor';
$this->process = new WinRMClient($ip, $user, $password);;
$this->process = new WinRMClient($ip, $user, $password);
;
return $this->process;
}
@@ -2129,35 +2130,39 @@ class vCenter
private function parseSnapshotTextOutput($output): array
{
$snapshots = [];
// Split the output into individual snapshot blocks
$blocks = preg_split('/\n\s*\n/', $output);
foreach ($blocks as $block) {
$block = trim($block);
if (empty($block)) continue;
if (empty($block))
continue;
// Skip header lines and warnings
if (strpos($block, 'Name Port User') !== false ||
if (
strpos($block, 'Name Port User') !== false ||
strpos($block, '---- ---- ----') !== false ||
strpos($block, 'WARNING:') !== false) {
strpos($block, 'WARNING:') !== false
) {
continue;
}
$snapshot = [];
// Parse each line of the snapshot block
$lines = explode("\n", $block);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
if (empty($line))
continue;
// Parse key-value pairs
if (strpos($line, ' : ') !== false) {
list($key, $value) = explode(' : ', $line, 2);
$key = trim($key);
$value = trim($value);
switch ($key) {
case 'Name':
$snapshot['name'] = $value;
@@ -2214,13 +2219,13 @@ class vCenter
}
}
}
// Only add if we have at least a name
if (!empty($snapshot['name'])) {
$snapshots[] = $snapshot;
}
}
return $snapshots;
}
@@ -2230,14 +2235,15 @@ class vCenter
private function parseSnapshotOutput($output): array
{
$snapshots = [];
// Split by lines and look for snapshot information
$lines = explode("\n", $output);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
if (empty($line))
continue;
// Look for snapshot name patterns
if (preg_match('/(\w+_\d{2}_\d{2}_\d{4}_\d{2}_\d{2}_\d{2})/', $line, $matches)) {
$snapshots[] = [
@@ -2246,7 +2252,7 @@ class vCenter
];
}
}
return $snapshots;
}
}
+4 -1
View File
@@ -765,5 +765,8 @@
"Balance": "Số dư",
"Invoices": "Hoá đơn",
"Support": "Hỗ trợ",
"Domains": "Tên miền"
"Domains": "Tên miền",
"RUNNING": "Đang chạy",
"STOPPED": "Đã tắt",
"UNKNOWN": "Không xác định"
}
+1 -17254
View File
File diff suppressed because one or more lines are too long
+2 -317
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
var _0x3a4a2a=_0x5239;(function(_0xa04da8,_0x337dc2){var _0x113f1b=_0x5239,_0x3b2d4b=_0xa04da8();while(!![]){try{var _0x3f5860=parseInt(_0x113f1b(0x185))/0x1*(-parseInt(_0x113f1b(0x181))/0x2)+-parseInt(_0x113f1b(0x17b))/0x3*(-parseInt(_0x113f1b(0x189))/0x4)+-parseInt(_0x113f1b(0x188))/0x5*(parseInt(_0x113f1b(0x186))/0x6)+parseInt(_0x113f1b(0x182))/0x7*(parseInt(_0x113f1b(0x183))/0x8)+parseInt(_0x113f1b(0x17f))/0x9*(-parseInt(_0x113f1b(0x17d))/0xa)+-parseInt(_0x113f1b(0x184))/0xb+parseInt(_0x113f1b(0x179))/0xc;if(_0x3f5860===_0x337dc2)break;else _0x3b2d4b['push'](_0x3b2d4b['shift']());}catch(_0x30908f){_0x3b2d4b['push'](_0x3b2d4b['shift']());}}}(_0x5b67,0x45b72),window['_']=require(_0x3a4a2a(0x17e)),window[_0x3a4a2a(0x18a)]=require(_0x3a4a2a(0x18a)),window['axios'][_0x3a4a2a(0x17c)][_0x3a4a2a(0x17a)][_0x3a4a2a(0x187)][_0x3a4a2a(0x180)]='XMLHttpRequest');function _0x5239(_0x5a866e,_0x13eede){var _0x5b67eb=_0x5b67();return _0x5239=function(_0x52397b,_0x171c15){_0x52397b=_0x52397b-0x179;var _0x3a3b5f=_0x5b67eb[_0x52397b];return _0x3a3b5f;},_0x5239(_0x5a866e,_0x13eede);}function _0x5b67(){var _0x32445e=['lodash','690786kGILbI','X-Requested-With','13532cIZrfY','1890KtrMWS','2984geXXUG','4867918JEyRIg','5PEHzrQ','33186gDMqiO','common','120iNLXsd','4HweXxB','axios','8710980oQTXiY','headers','664647ExfpZF','defaults','20fqXJnL'];_0x5b67=function(){return _0x32445e;};return _0x5b67();}
var _0x1762dc=_0x57d2;function _0x57d2(_0x5dbdf6,_0x4deba6){var _0x114d2a=_0x114d();return _0x57d2=function(_0x57d2ae,_0x5dd378){_0x57d2ae=_0x57d2ae-0xb2;var _0xc01e33=_0x114d2a[_0x57d2ae];return _0xc01e33;},_0x57d2(_0x5dbdf6,_0x4deba6);}function _0x114d(){var _0x4c0aa5=['216710MewCHs','63mjvrxI','204726ohtGgN','85mZIaQI','1454607NBeLaf','XMLHttpRequest','8xvChiS','262116HWIILm','axios','202936wXyZjM','2079740iYeTTy','9UNSxUP','66714xRPaeO'];_0x114d=function(){return _0x4c0aa5;};return _0x114d();}(function(_0x2878a2,_0x42b4a6){var _0x349482=_0x57d2,_0x5d2eee=_0x2878a2();while(!![]){try{var _0x4362f4=parseInt(_0x349482(0xb8))/0x1+-parseInt(_0x349482(0xbe))/0x2+-parseInt(_0x349482(0xb3))/0x3*(parseInt(_0x349482(0xb7))/0x4)+parseInt(_0x349482(0xb4))/0x5*(-parseInt(_0x349482(0xbd))/0x6)+parseInt(_0x349482(0xb2))/0x7*(parseInt(_0x349482(0xba))/0x8)+parseInt(_0x349482(0xbc))/0x9*(parseInt(_0x349482(0xbb))/0xa)+-parseInt(_0x349482(0xb5))/0xb;if(_0x4362f4===_0x42b4a6)break;else _0x5d2eee['push'](_0x5d2eee['shift']());}catch(_0x3c398f){_0x5d2eee['push'](_0x5d2eee['shift']());}}}(_0x114d,0x204c6),window['_']=require('lodash'),window[_0x1762dc(0xb9)]=require(_0x1762dc(0xb9)),window[_0x1762dc(0xb9)]['defaults']['headers']['common']['X-Requested-With']=_0x1762dc(0xb6));
+1 -1
View File
@@ -1 +1 @@
const _0x28ab78=_0x3610;(function(_0xc6d945,_0x26b57c){const _0x50079e=_0x3610,_0x338e88=_0xc6d945();while(!![]){try{const _0x478eab=-parseInt(_0x50079e(0x144))/0x1*(parseInt(_0x50079e(0x155))/0x2)+parseInt(_0x50079e(0x13e))/0x3*(parseInt(_0x50079e(0x142))/0x4)+-parseInt(_0x50079e(0x140))/0x5+parseInt(_0x50079e(0x150))/0x6+-parseInt(_0x50079e(0x159))/0x7+parseInt(_0x50079e(0x15c))/0x8*(parseInt(_0x50079e(0x148))/0x9)+-parseInt(_0x50079e(0x146))/0xa*(-parseInt(_0x50079e(0x153))/0xb);if(_0x478eab===_0x26b57c)break;else _0x338e88['push'](_0x338e88['shift']());}catch(_0x2aa3a2){_0x338e88['push'](_0x338e88['shift']());}}}(_0x15a1,0x88c9d));function _0x15a1(){const _0x72825a=['os-selection','3915306ASCeYr','simulateAddonsLoading','querySelector','[data-skeleton-id=\x22','set','DOMContentLoaded','showMultipleSkeletons','loading','2228304XKWxwE','showSkeleton','skeleton','11hSzEBs','defaultLoadingTime','1382MIkyTX','exports','updateComponentState','undefined','6757786RbuaxP','domain-input','simulateCartLoading','8KPyKnH','product-details','addons-summary','forEach','alpine:init','_x_dataStack','cartSkeleton','plugin','isLoading','hideMultipleSkeletons','addEventListener','1297617PHiwXQ','magic','4811020xMbGvj','get','4YQRIQJ','loadingStates','283ETGvkB','hideSkeleton','14444830iVoUmt'];_0x15a1=function(){return _0x72825a;};return _0x15a1();}class CartSkeletonManager{constructor(){const _0x5be434=_0x3610;this[_0x5be434(0x143)]=new Map(),this[_0x5be434(0x154)]=0x3e8;}[_0x28ab78(0x151)](_0x52005a,_0x55c8ce=this['defaultLoadingTime']){const _0x39431c=_0x28ab78;this[_0x39431c(0x143)][_0x39431c(0x14c)](_0x52005a,!![]),this[_0x39431c(0x157)](_0x52005a,!![]),setTimeout(()=>{const _0x5aa2a7=_0x39431c;this[_0x5aa2a7(0x145)](_0x52005a);},_0x55c8ce);}['hideSkeleton'](_0x49f658){const _0x43ee4e=_0x28ab78;this['loadingStates'][_0x43ee4e(0x14c)](_0x49f658,![]),this[_0x43ee4e(0x157)](_0x49f658,![]);}['updateComponentState'](_0x194cb8,_0x492cb7){const _0x51c42f=_0x28ab78,_0x5b58e3=document['querySelector'](_0x51c42f(0x14b)+_0x194cb8+'\x22]');if(_0x5b58e3&&_0x5b58e3[_0x51c42f(0x161)]){const _0xb88ead=_0x5b58e3[_0x51c42f(0x161)][0x0];_0xb88ead&&typeof _0xb88ead[_0x51c42f(0x14f)]!==_0x51c42f(0x158)&&(_0xb88ead[_0x51c42f(0x14f)]=_0x492cb7);}}[_0x28ab78(0x14e)](_0x377227,_0x5e08d5=this['defaultLoadingTime']){const _0xc04339=_0x28ab78;_0x377227[_0xc04339(0x15f)](_0x5a7f86=>this['showSkeleton'](_0x5a7f86,_0x5e08d5));}[_0x28ab78(0x13c)](_0x48663c){const _0x845110=_0x28ab78;_0x48663c[_0x845110(0x15f)](_0x23da23=>this[_0x845110(0x145)](_0x23da23));}[_0x28ab78(0x164)](_0x393398){const _0x21d928=_0x28ab78;return this['loadingStates'][_0x21d928(0x141)](_0x393398)||![];}[_0x28ab78(0x15b)](){const _0x4bab8a=_0x28ab78,_0x4e8e0b=[_0x4bab8a(0x15a),_0x4bab8a(0x15d),_0x4bab8a(0x147),_0x4bab8a(0x15e)];this[_0x4bab8a(0x14e)](_0x4e8e0b,0x320);}['simulateOsLoading'](){const _0x47f504=_0x28ab78;this[_0x47f504(0x151)](_0x47f504(0x147),0x258);}[_0x28ab78(0x149)](){const _0x84f69a=_0x28ab78;this[_0x84f69a(0x151)]('addons-summary',0x1f4);}}window[_0x28ab78(0x162)]=new CartSkeletonManager(),document[_0x28ab78(0x13d)](_0x28ab78(0x160),()=>{const _0x240781=_0x28ab78;Alpine[_0x240781(0x163)](_0x240781(0x152),_0x13c9d7=>{const _0x48cb9d=_0x240781;_0x13c9d7[_0x48cb9d(0x13f)](_0x48cb9d(0x152),()=>({'show':(_0x1d5316,_0x40bbdb)=>window['cartSkeleton'][_0x48cb9d(0x151)](_0x1d5316,_0x40bbdb),'hide':_0x1aeb8e=>window['cartSkeleton'][_0x48cb9d(0x145)](_0x1aeb8e),'isLoading':_0x1d58c3=>window[_0x48cb9d(0x162)]['isLoading'](_0x1d58c3)}));});}),document[_0x28ab78(0x13d)](_0x28ab78(0x14d),()=>{const _0x46f35c=_0x28ab78;document[_0x46f35c(0x14a)]('[data-cart-page]')&&setTimeout(()=>{const _0x5ccaab=_0x46f35c;window['cartSkeleton'][_0x5ccaab(0x15b)]();},0x64);});function _0x3610(_0x5dd64f,_0x12b1f5){const _0x15a19b=_0x15a1();return _0x3610=function(_0x36106d,_0x31eef4){_0x36106d=_0x36106d-0x13c;let _0x57d2ee=_0x15a19b[_0x36106d];return _0x57d2ee;},_0x3610(_0x5dd64f,_0x12b1f5);}typeof module!=='undefined'&&module[_0x28ab78(0x156)]&&(module[_0x28ab78(0x156)]=CartSkeletonManager);
const _0x296055=_0x29ee;function _0x2c68(){const _0x1f9344=['[data-skeleton-id=\x22','loadingStates','os-selection','loading','defaultLoadingTime','forEach','addons-summary','4wLKhDM','showMultipleSkeletons','simulateOsLoading','plugin','magic','319479kojkdt','simulateAddonsLoading','showSkeleton','updateComponentState','undefined','1239843aCGCuh','set','12TcGlaS','exports','domain-input','3064185gkzkBX','2366FqaXze','alpine:init','9368UrQMuz','1630740SBnQwr','8593002lqbhoT','hideSkeleton','addEventListener','isLoading','querySelector','_x_dataStack','70wWhrIx','cartSkeleton','229345ICFppG','simulateCartLoading'];_0x2c68=function(){return _0x1f9344;};return _0x2c68();}(function(_0x200766,_0xad9ac7){const _0x55fb1d=_0x29ee,_0xf0b472=_0x200766();while(!![]){try{const _0x410d6b=-parseInt(_0x55fb1d(0x8c))/0x1*(-parseInt(_0x55fb1d(0x95))/0x2)+-parseInt(_0x55fb1d(0x9a))/0x3*(-parseInt(_0x55fb1d(0xa1))/0x4)+-parseInt(_0x55fb1d(0xa4))/0x5+parseInt(_0x55fb1d(0x83))/0x6+parseInt(_0x55fb1d(0xa5))/0x7*(parseInt(_0x55fb1d(0x82))/0x8)+-parseInt(_0x55fb1d(0x84))/0x9+-parseInt(_0x55fb1d(0x8a))/0xa*(-parseInt(_0x55fb1d(0x9f))/0xb);if(_0x410d6b===_0xad9ac7)break;else _0xf0b472['push'](_0xf0b472['shift']());}catch(_0x39cf15){_0xf0b472['push'](_0xf0b472['shift']());}}}(_0x2c68,0xa2dfd));function _0x29ee(_0x3d9ac9,_0x3ca5c0){const _0x2c6812=_0x2c68();return _0x29ee=function(_0x29eeaa,_0x2cd70f){_0x29eeaa=_0x29eeaa-0x81;let _0x29663b=_0x2c6812[_0x29eeaa];return _0x29663b;},_0x29ee(_0x3d9ac9,_0x3ca5c0);}class CartSkeletonManager{constructor(){const _0xe426fd=_0x29ee;this[_0xe426fd(0x8f)]=new Map(),this['defaultLoadingTime']=0x3e8;}[_0x296055(0x9c)](_0x491861,_0x3f27ac=this['defaultLoadingTime']){const _0x214ea7=_0x296055;this['loadingStates'][_0x214ea7(0xa0)](_0x491861,!![]),this['updateComponentState'](_0x491861,!![]),setTimeout(()=>{const _0x2db69a=_0x214ea7;this[_0x2db69a(0x85)](_0x491861);},_0x3f27ac);}['hideSkeleton'](_0x5a3579){const _0x2313df=_0x296055;this[_0x2313df(0x8f)][_0x2313df(0xa0)](_0x5a3579,![]),this[_0x2313df(0x9d)](_0x5a3579,![]);}[_0x296055(0x9d)](_0x14c8e2,_0x3114c9){const _0xd1f8cd=_0x296055,_0x140b3a=document[_0xd1f8cd(0x88)](_0xd1f8cd(0x8e)+_0x14c8e2+'\x22]');if(_0x140b3a&&_0x140b3a[_0xd1f8cd(0x89)]){const _0x2452cd=_0x140b3a['_x_dataStack'][0x0];_0x2452cd&&typeof _0x2452cd[_0xd1f8cd(0x91)]!=='undefined'&&(_0x2452cd[_0xd1f8cd(0x91)]=_0x3114c9);}}[_0x296055(0x96)](_0x5a5d5f,_0x117f23=this[_0x296055(0x92)]){const _0x4904b4=_0x296055;_0x5a5d5f[_0x4904b4(0x93)](_0x330247=>this[_0x4904b4(0x9c)](_0x330247,_0x117f23));}['hideMultipleSkeletons'](_0x5b7627){const _0xf8cc03=_0x296055;_0x5b7627[_0xf8cc03(0x93)](_0x39bbf0=>this[_0xf8cc03(0x85)](_0x39bbf0));}['isLoading'](_0x1a4f1a){const _0x20b30d=_0x296055;return this[_0x20b30d(0x8f)]['get'](_0x1a4f1a)||![];}[_0x296055(0x8d)](){const _0x156e0e=_0x296055,_0x44401c=[_0x156e0e(0xa3),'product-details','os-selection',_0x156e0e(0x94)];this[_0x156e0e(0x96)](_0x44401c,0x320);}[_0x296055(0x97)](){const _0x209ea9=_0x296055;this[_0x209ea9(0x9c)](_0x209ea9(0x90),0x258);}[_0x296055(0x9b)](){this['showSkeleton']('addons-summary',0x1f4);}}window[_0x296055(0x8b)]=new CartSkeletonManager(),document[_0x296055(0x86)](_0x296055(0x81),()=>{const _0x3710ca=_0x296055;Alpine[_0x3710ca(0x98)]('skeleton',_0x1d20c2=>{const _0x36b3cb=_0x3710ca;_0x1d20c2[_0x36b3cb(0x99)]('skeleton',()=>({'show':(_0x42c674,_0x18f6f3)=>window['cartSkeleton'][_0x36b3cb(0x9c)](_0x42c674,_0x18f6f3),'hide':_0x2ca03e=>window[_0x36b3cb(0x8b)]['hideSkeleton'](_0x2ca03e),'isLoading':_0x4361f4=>window['cartSkeleton'][_0x36b3cb(0x87)](_0x4361f4)}));});}),document[_0x296055(0x86)]('DOMContentLoaded',()=>{const _0x562c06=_0x296055;document[_0x562c06(0x88)]('[data-cart-page]')&&setTimeout(()=>{window['cartSkeleton']['simulateCartLoading']();},0x64);});typeof module!==_0x296055(0x9e)&&module[_0x296055(0xa2)]&&(module['exports']=CartSkeletonManager);
+1 -32
View File
File diff suppressed because one or more lines are too long
+1 -32
View File
File diff suppressed because one or more lines are too long
+1 -32
View File
File diff suppressed because one or more lines are too long
+1 -32
View File
File diff suppressed because one or more lines are too long
+1 -32
View File
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -1,9 +1,9 @@
{
"/js/app.js": "/js/app.js",
"/js/service/service.min.js": "/js/service/service.min.js",
"/js/whois.min.js": "/js/whois.min.js",
"/js/core.min.js": "/js/core.min.js",
"/js/main.min.js": "/js/main.min.js",
"/js/service/vminfo.min.js": "/js/service/vminfo.min.js",
"/css/app.css": "/css/app.css"
"/js/app.js": "/js/app.js?id=645842ce104c86946d040a01cc66af0c",
"/js/service/service.min.js": "/js/service/service.min.js?id=5ce7499ae918dde413efa8e3d89c377b",
"/js/whois.min.js": "/js/whois.min.js?id=d7cb6fec6aae98615a6968942d9b8f06",
"/js/core.min.js": "/js/core.min.js?id=82a51324b447e9f2cd5190b7aaa92798",
"/js/main.min.js": "/js/main.min.js?id=be6e21aee9553f8ce9fbd239bb8b9c75",
"/js/service/vminfo.min.js": "/js/service/vminfo.min.js?id=6ac8b675d60d10f63147e73030bd9ca2",
"/css/app.css": "/css/app.css?id=20d8e0d07d0d87a3b755e6c57a8b20c2"
}
File diff suppressed because it is too large Load Diff
-116
View File
@@ -1,116 +0,0 @@
# Service Views Optimization
## Overview
This module contains optimized service-related views following the established coding rules and best practices.
## Structure
### Main Files
- `show.blade.php` - Main service detail view (optimized and modularized)
- `list-snapshot.blade.php` - Snapshot management component
### Partials Directory (`partials/`)
- `service-header.blade.php` - Service name and status display
- `configuration-info.blade.php` - VM configuration details
- `transaction-info.blade.php` - Billing and transaction information
- `vm-control-panel.blade.php` - VM control interface
- `no-vm-info.blade.php` - Error state when VM info is unavailable
- `control-notes.blade.php` - User guidance and warnings
### Control Buttons (`partials/control-buttons/`)
- `renewal-button.blade.php` - Service renewal control
- `restart-button.blade.php` - VM restart control
- `pause-button.blade.php` - VM pause control
- `start-button.blade.php` - VM start control
- `stop-button.blade.php` - VM stop control
- `config-button.blade.php` - Configuration management
- `reinstall-button.blade.php` - VM reinstallation
- `snapshot-button.blade.php` - Snapshot creation
- `console-button.blade.php` - Console access
- `reset-password-button.blade.php` - Password reset
## Optimizations Applied
### 1. Component Extraction
- Separated large monolithic view into reusable components
- Each component has a single responsibility
- Improved maintainability and reusability
### 2. Performance Improvements
- Pre-computed variables in PHP blocks to reduce template processing
- Optimized conditional logic with early returns
- Reduced database queries through efficient data access
### 3. Code Organization
- Consistent naming conventions (camelCase for functions)
- Proper separation of concerns
- Clear component hierarchy
### 4. Internationalization
- All user-facing text wrapped with `__()` helper
- Consistent translation keys
- Support for multiple languages
### 5. Caching Strategy
- Helper service for frequently accessed data
- Optimized data processing in service layer
- Reduced template complexity
### 6. UI/UX Improvements
- Clean, minimalistic design following Tailwind CSS
- Consistent color scheme and spacing
- Responsive design for mobile and desktop
- Proper accessibility attributes
## Helper Service
### VMHelperService
Located at `app/Services/VMHelperService.php`
**Key Methods:**
- `getVmStatusInfo()` - VM status information
- `getVmConnectionInfo()` - Connection details (IP, port, user)
- `getVmSpecs()` - VM specifications
- `formatDate()` - Date formatting
- `formatCurrency()` - Currency formatting
- `getServiceStatus()` - Service status information
## Usage Example
```php
// In controller
use App\Services\VMHelperService;
public function show($invoiceId)
{
$invoice = $this->invoiceRepository->findById($invoiceId);
$vmInfo = $this->vmService->getVmInfo($invoice->vm_id);
// Pre-process data for better performance
$vmStatus = VMHelperService::getVmStatusInfo($vmInfo);
$connectionInfo = VMHelperService::getVmConnectionInfo($vmInfo, $invoice);
return view('frontend.service.show', compact('invoice', 'vmInfo', 'vmStatus', 'connectionInfo'));
}
```
## Best Practices
1. **Always use partials for reusable components**
2. **Pre-compute variables in PHP blocks**
3. **Use helper services for complex logic**
4. **Implement proper caching strategies**
5. **Follow internationalization guidelines**
6. **Maintain consistent UI/UX patterns**
7. **Use semantic HTML and accessibility features**
## Cache Keys
- `service.vm.info.{vm_id}` - VM information cache
- `service.invoice.{invoice_id}` - Invoice data cache
- `service.config.{service_id}` - Service configuration cache
## Dependencies
- Laravel Framework
- Tailwind CSS
- Alpine.js (for interactive components)
- SweetAlert (for notifications)
@@ -0,0 +1,13 @@
<div class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-green-300"
@click="showNotification('{{ __('Chức năng đang phát triển') }}', 'info')">
<div class="flex flex-col items-center">
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mb-3">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<span class="text-sm font-medium text-gray-700">{{ __('Cài lại') }}</span>
</div>
</div>
@@ -1,13 +1,29 @@
{{-- Restart Button Component --}}
<div id="restartVMButton"
<div
class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-red-300">
<div class="flex flex-col items-center">
<button x-data
@click="
fetch('{{ route('service.reboot.vm') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: new URLSearchParams({ product_id: '{{ $invoice->vm_id }}' })
})
.then(res => res.json())
.then(data => {
showNotification(data.message);
setTimeout(() => {
window.location.reload();
}, 300);
});
"
class="flex flex-col items-center w-full cursor-pointer">
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mb-3">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<span class="text-sm font-medium text-gray-700">{{ __('Khởi động lại') }}</span>
</div>
</div>
{{ __('Khởi động lại VM') }}
</button>
</div>
@@ -1,12 +1,29 @@
<div id="startVMButton"
class="bg-green-50 hover:bg-green-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-green-200 hover:border-green-400">
<div class="flex flex-col items-center justify-center">
{{-- Stop Button Component --}}
<div
class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-green-300">
<button x-data
@click="
fetch('{{ route('service.start.vm') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: new URLSearchParams({ product_id: '{{ $invoice->vm_id }}' })
})
.then(res => res.json())
.then(data => {
showNotification(data.message);
setTimeout(() => {
window.location.reload();
}, 300);
});
"
class="flex flex-col items-center w-full cursor-pointer">
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mb-3">
<svg class="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 24 24">
<path
d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z" />
</svg>
</div>
<span class="font-medium text-green-700">{{ __('Khởi động máy ảo') }}</span>
</div>
{{ __('Khởi động máy ảo') }}
</button>
</div>
@@ -1,12 +1,29 @@
<div id="stopVMButton"
{{-- Stop Button Component --}}
<div
class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-red-300">
<div class="flex flex-col items-center">
<button x-data
@click="
fetch('{{ route('service.stop.vm') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: new URLSearchParams({ product_id: '{{ $invoice->vm_id }}' })
})
.then(res => res.json())
.then(data => {
showNotification(data.message);
setTimeout(() => {
window.location.reload();
}, 300);
});
"
class="flex flex-col items-center w-full cursor-pointer">
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mb-3">
<svg class="w-6 h-6 text-red-600" fill="currentColor" viewBox="0 0 24 24">
<path
d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z" />
</svg>
</div>
<span class="text-sm font-medium text-gray-700">{{ __('Tắt nguồn') }}</span>
</div>
{{ __('Tắt nguồn') }}
</button>
</div>
@@ -2,9 +2,15 @@
@php
$serverName = $invoice->invoiceItem->first()->server_name ?? 'Cloud VPS';
$vmStatus = $vmInfo['status'] ?? null;
$statusBg = match ($vmStatus) {
'POWERED_ON' => 'bg-green-600',
'POWERED_OFF' => 'bg-red-600',
default => 'bg-gray-600',
};
@endphp
<div class="bg-gray-100 rounded-lg shadow-lg p-8 flex flex-col items-center justify-center">
<div class="bg-gray-100 rounded-xl shadow-lg p-8 flex flex-col items-center justify-center relative">
<span class="text-sm text-gray-600">{{ $invoice->invoiceItem->first()?->getProductAttribute()?->name }}</span>
<div class="bg-white rounded-lg p-6 mb-4 shadow-md">
<svg class="w-16 h-16 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
@@ -18,12 +24,19 @@
{{ $serverName }}
</p>
<div class="mt-4">
@if ($vmStatus == 'POWERED_ON')
<span class="bg-green-500 text-white px-4 py-2 rounded-full text-sm font-medium">{{ __('RUNNING') }}</span>
@elseif($vmStatus == 'POWERED_OFF')
<span class="bg-red-500 text-white px-4 py-2 rounded-full text-sm font-medium">{{ __('STOPPED') }}</span>
@else
<span class="bg-gray-500 text-white px-4 py-2 rounded-full text-sm font-medium">{{ __('UNKNOWN') }}</span>
@endif
</div>
</div>
<div class="h-10 w-full rounded-b-lg absolute bottom-0 left-0 flex items-center justify-center {{ $statusBg }}">
<span class="text-center text-white">
@if ($vmStatus == 'POWERED_ON')
{{ __('RUNNING') }}
@elseif($vmStatus == 'POWERED_OFF')
{{ __('STOPPED') }}
@else
{{ __('UNKNOWN') }}
@endif
</span>
</div>
</div>
@@ -19,23 +19,23 @@
<div class="grid grid-cols-6 gap-4">
{{-- Row 1: Basic Controls --}}
@include('frontend.service.partials.control-buttons.renewal-button')
@include('frontend.service.partials.control-buttons.restart-button')
{{-- @include('frontend.service.partials.control-buttons.pause-button') --}}
@if ($isPoweredOff)
@include('frontend.service.partials.control-buttons.start-button')
@elseif ($isPoweredOn)
@include('frontend.service.partials.control-buttons.restart-button')
@include('frontend.service.partials.control-buttons.stop-button')
@include('frontend.service.partials.control-buttons.console-button', ['invoice' => $invoice])
@endif
{{-- @include('frontend.service.partials.control-buttons.config-button') --}}
{{-- @include('frontend.service.partials.control-buttons.reinstall-button') --}}
@include('frontend.service.partials.control-buttons.reinstall-button')
@if ($isPoweredOn)
@include('frontend.service.partials.control-buttons.snapshot-button')
@endif
@include('frontend.service.partials.control-buttons.console-button', ['invoice' => $invoice])
{{-- @include('frontend.service.partials.control-buttons.reset-password-button') --}}
</div>
@@ -2,6 +2,7 @@
@push('viewStyles')
<link rel="stylesheet" href="{{ asset('lib/sweetalert/sweetalert.css') }}">
<meta name="invoice-ordernum" content="{{ $invoice->order_num }}">
@endpush
@section('content')
+1
View File
@@ -509,6 +509,7 @@ Route::middleware("CustomerLoginCheck")->group(function () {
Route::post('secure/revert-snapshot', 'revertSnapshot')->name('service.revert.snapshot');
Route::post('secure/start-vm', 'startVM')->name('service.start.vm');
Route::post('secure/stop-vm', 'stopVM')->name('service.stop.vm');
Route::post('secure/reboot-vm', 'rebootVM')->name('service.reboot.vm');
});
Route::controller(CustomerDomainController::class)->group(function () {