From 197fe413e5b3b9f6d27feaa70ca8844afdd0014c Mon Sep 17 00:00:00 2001 From: pbaodev Date: Sun, 24 Aug 2025 22:41:15 +0700 Subject: [PATCH] update change password --- .../frontend/ServiceController.php | 152 ++++--------- app/Http/Services/vCenter.php | 175 ++++++++++++-- .../Action/ResetVmPasswordJob.php | 101 +++++++++ .../Create/CreateVirtualMachineJob.php | 49 ++-- .../VirtualMachine/Sync/SyncAllPackage.php | 10 +- app/Jobs/VirtualMachine/Sync/SyncNetwork.php | 2 +- app/Mail/VmPasswordResetSuccess.php | 23 ++ app/Providers/HorizonServiceProvider.php | 36 +++ app/Services/Cache/CacheService.php | 57 +++++ app/Services/Cache/CacheServiceInterface.php | 11 + config/app.php | 1 + config/horizon.php | 213 ++++++++++++++++++ ...2025_08_03_223132_update_invoice_table.php | 28 --- database/seeders/DatabaseSeeder.php | 1 + resources/views/admin/form/vps.blade.php | 2 - .../vm_password_reset_success.blade.php | 5 + .../partials/configuration-info.blade.php | 2 +- .../partials/reset-password-modal.blade.php | 19 +- .../partials/vm-control-panel.blade.php | 5 +- 19 files changed, 699 insertions(+), 193 deletions(-) create mode 100644 app/Jobs/VirtualMachine/Action/ResetVmPasswordJob.php create mode 100644 app/Mail/VmPasswordResetSuccess.php create mode 100644 app/Providers/HorizonServiceProvider.php create mode 100644 app/Services/Cache/CacheService.php create mode 100644 app/Services/Cache/CacheServiceInterface.php create mode 100644 config/horizon.php delete mode 100644 database/migrations/2025_08_03_223132_update_invoice_table.php create mode 100644 resources/views/emails/vm_password_reset_success.blade.php diff --git a/app/Http/Controllers/frontend/ServiceController.php b/app/Http/Controllers/frontend/ServiceController.php index fa359be..7475a83 100644 --- a/app/Http/Controllers/frontend/ServiceController.php +++ b/app/Http/Controllers/frontend/ServiceController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\frontend; use App\Http\Controllers\Controller; use App\Http\Requests\Vm\SnapShotRequest; +use App\Jobs\VirtualMachine\Action\ResetVmPasswordJob; use App\Models\Invoice; use App\Models\InvoiceItem; use App\Repositories\Invoice\InvoiceRepositoryInterface; @@ -552,109 +553,50 @@ class ServiceController extends Controller /** * Reset VM password (Windows/Linux) via vCenter helpers */ - public function resetVMPassword(Request $request, vCenter $serviceVCenter) + public function resetVMPassword(Request $request) { - 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); - } - - $validated = FacadesValidator::make($request->all(), [ - 'product_id' => 'required|string|max:255', - 'new_password' => 'required|string|min:8|max:128', - ]); - - if ($validated->fails()) { - return response()->json([ - 'error' => true, - 'message' => __('Dữ liệu không hợp lệ'), - 'errors' => $validated->errors(), - ], 422); - } - - $productId = $this->sanitizeInput($request->product_id); - $newPassword = (string) $request->new_password; - - // Load product and ensure ownership - $product = ModuleVmware::with('ipAddress.host.datacenter.vCenter') - ->where('vmware_id', $productId) - ->where('customer_id', $user->id) - ->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); - } - - $vcenter = $product->ipAddress->host->datacenter->vCenter ?? null; - if (!$vcenter) { - return response()->json([ - 'error' => true, - 'message' => __('Cấu hình vCenter không hợp lệ'), - ], 400); - } - - // Login to vCenter - if (!$serviceVCenter->loginServer($vcenter)) { - return response()->json([ - 'error' => true, - 'message' => __('Không thể kết nối đến vCenter'), - ], 500); - } - - // Determine OS family - $osFamily = strtoupper($product->os->osGroup->name ?? ''); - $isWindows = str_contains($osFamily, 'WINDOWS'); - - // Prepare data - $ip = $product->ipAddress->ip ?? null; - if (!$ip) { - return response()->json([ - 'error' => true, - 'message' => __('Không tìm thấy IP của máy ảo'), - ], 400); - } - - $payload = [ - 'ip' => $ip, - 'user' => $isWindows ? 'administrator' : 'root', - 'password' => $product->password ?? 'TGS@123', // fallback if field exists; adjust per model - 'new_password' => $newPassword, - ]; - - $ok = $isWindows - ? $serviceVCenter->change_password_windows($payload) - : $serviceVCenter->change_password_linux($payload); - - if (!$ok) { - return response()->json([ - 'error' => true, - 'message' => __('Không thể đặt lại mật khẩu. Vui lòng thử lại sau.'), - ], 500); - } - - return response()->json([ - 'error' => false, - 'message' => __('Đặt lại mật khẩu thành công'), - ]); - } catch (\Exception $e) { - Log::error('Lỗi reset VM password', [ - 'product_id' => $request->product_id ?? null, - 'error' => $e->getMessage(), - ]); - return response()->json([ - 'error' => true, - 'message' => __('Có lỗi xảy ra khi đặt lại mật khẩu'), - ], 500); + $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); } + + $validated = Validator::make($request->all(), [ + 'product_id' => 'required|string|max:255', + 'new_password' => 'required|string|min:8|max:128', + 'old_password' => 'required|string|min:8|max:128', + ]); + if ($validated->fails()) { + return response()->json(['error' => true, 'message' => __('Dữ liệu không hợp lệ'), 'errors' => $validated->errors()], 422); + } + + $productId = $this->sanitizeInput($request->product_id); + $newPassword = (string) $request->new_password; + $oldPassword = (string) $request->old_password; + + // load & ownership check nhanh (không cần toàn bộ nặng nề vì Job sẽ retry) + $product = ModuleVmware::where('vmware_id', $productId) + ->where('customer_id', $user->id) + ->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); + } + + // Dispatch job với delay 5s + ResetVmPasswordJob::dispatch( + productId: $productId, + newPassword: $newPassword, + oldPassword: $oldPassword, + userId: $user->id, + email: $user->email + )->delay(now()->addSeconds(5)); + + // trả accepted để UI biết đang xử lý nền + return redirect()->back()->with('success', 'Đã gửi yêu cầu tạo đặt lại mật khẩu. Quá trình sẽ mất 3-5 phút, vui lòng chờ đợi.'); } + + + /** * Change Remote Desktop port (Windows only) */ @@ -827,8 +769,8 @@ class ServiceController extends Controller // Fetch product price based on invoice item link $invoiceItem = InvoiceItem::whereHas('invoice', function ($q) use ($user) { - $q->where('customer_id', $user->id); - }) + $q->where('customer_id', $user->id); + }) ->where('product_type', 4) // VPS ->whereHas('invoice', fn($q) => $q->where('status', '!=', 0)) ->first(); @@ -1131,7 +1073,7 @@ class ServiceController extends Controller ], 401); } - $validator = \Validator::make($request->all(), [ + $validator = Validator::make($request->all(), [ 'product_id' => 'required|string|max:255', ]); if ($validator->fails()) { @@ -1158,7 +1100,7 @@ class ServiceController extends Controller $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); + Cache::forget('the_gioi_so_database_the_gioi_so_cache: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.') @@ -1287,9 +1229,9 @@ class ServiceController extends Controller // Debug: Log invoice lookup Log::info('Looking for invoice', ['invoice_id' => $request->invoice_id]); - + $invoice = Invoice::find($request->invoice_id); - + // Debug: Log invoice result Log::info('Invoice lookup result', [ 'invoice_id' => $request->invoice_id, diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index b8a3323..139a0b3 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -15,7 +15,6 @@ use phpseclib3\Net\SSH2; class vCenter { - protected $process = null; public ?string $token = null; public string $apiUri = '10.10.10.6'; private string $username = 'administrator@vsphere.local'; @@ -23,7 +22,8 @@ class vCenter private int $diskCapacity = 1073741824; private int $memory = 1024; - protected string $os; + private ?string $os = null; + private SSH2|WinRMClient|null $process = null; public function loginServer(\App\Models\Vcenter $vcenterModel): bool|string { @@ -325,6 +325,142 @@ class vCenter } } + // Login SSH + public function loginSSH(): bool + { + $conn = $this->LoginAuth(); + + if ($conn) { + Log::info("✅ Connected to vCenter via SSH successfully"); + return true; + } + + Log::error("❌ Failed to connect to vCenter via SSH"); + return false; + } + + + public function runPsCommand(string $psCommand): string + { + $auth = $this->LoginAuth(); + if (!$auth) + return "Login_Fail"; + + // gọi trực tiếp -Command, thu cả STDERR + $action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ' + . '-Command "' . str_replace('"', '\"', $psCommand) . '"'; + + try { + $stream = ssh2_exec($auth, $action); + if (!$stream) + throw new \RuntimeException("ssh2_exec failed"); + $err = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); + stream_set_blocking($stream, true); + stream_set_blocking($err, true); + $out = stream_get_contents($stream) ?: ''; + $errOut = stream_get_contents($err) ?: ''; + return $out . ($errOut ? "\n[STDERR]\n" . $errOut : ''); + } catch (\Throwable $e) { + return "Error: " . $e->getMessage(); + } + } + + + protected function CommandRSP(string $cmd) + { + $auth = $this->LoginAuth(); + if (!$auth) + return "Login_Fail"; + + // Escape để dùng trong chuỗi single-quoted của PowerShell + $esc = fn(string $v) => str_replace("'", "''", $v); + + $server = $esc($this->apiUri ?? ''); + $user = $esc($this->username ?? ''); + $pass = $esc($this->password ?? ''); + + // Ghép script PowerShell hoàn chỉnh, chạy trong 1 scriptblock + // Lưu ý: dùng placeholder rồi strtr để tránh lẫn dấu ngoặc + $psTemplate = <<<'PS' +$ProgressPreference = 'SilentlyContinue' +$InformationPreference = 'SilentlyContinue' +$WarningPreference = 'SilentlyContinue' +$ErrorActionPreference = 'Stop' + +Import-Module VMware.PowerCLI -ErrorAction Stop +Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null +Connect-VIServer -Server '' -User '' -Password '' | Out-Null + +# Thực thi lệnh công việc (đã được xây dựng sẵn, ví dụ: & 'C:\path\script.ps1' -Param 'Value' ... ) +& { + +} | Out-String +PS; + + // Chèn dữ liệu + $psScript = strtr($psTemplate, [ + '' => $server, + '' => $user, + '' => $pass, + '' => $cmd, // ở đây $cmd KHÔNG cần ngoặc kép bao ngoài + ]); + + // Encode UTF-16LE → Base64 cho -EncodedCommand + $encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8')); + + // Dùng -EncodedCommand để tránh mọi lỗi escape/quote + $action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded"; + + try { + $stream = ssh2_exec($auth, $action); + if (!$stream) + return "ssh2_exec failed"; + $err = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); + stream_set_blocking($stream, true); + stream_set_blocking($err, true); + $out = stream_get_contents($stream) ?: ''; + $errOut = stream_get_contents($err) ?: ''; + return $out . ($errOut ? "\n[STDERR]\n" . $errOut : ''); + } catch (\Throwable $e) { + return "Error: " . $e->getMessage(); + } + } + public function resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string + { + // File ps1 chạy được ngoài PowerShell: bạn đã test ok + $psFile = "C:\\CLISave\\AssignIP\\SetGuestPwd.ps1"; + + // Escape chuỗi cho PowerShell single-quoted + $q = fn(string $v) => str_replace("'", "''", $v); + + // Xây args kiểu: -Key 'Value' + $args = " -VMName '" . $q($vmName) . "'"; + $args .= " -GuestUser '" . $q($guestUser) . "'"; + $args .= " -GuestPassword '" . $q($guestPassword) . "'"; + $args .= " -TargetUser '" . $q($targetUser) . "'"; + $args .= " -NewPassword '" . $q($newPassword) . "'"; + + // Gọi file trong cùng phiên PS đã Connect-VIServer (nhờ Command()) + // Thêm Out-String để ép JSON nằm trên STDOUT gọn + $cmd = "& '$psFile'$args | Out-String"; + + $raw = $this->CommandRSP($cmd); // <-- dùng lại Command() của bạn + if ($raw === 'Login_Fail' || $raw === false || $raw === null) { + return "Login_Fail"; + } + + // Thử parse JSON (có thể lẫn banner/log) + $trim = trim($raw); + $data = json_decode($trim, true); + if (json_last_error() !== JSON_ERROR_NONE) { + if (preg_match('/(\{.*\}|\[.*\])/s', $trim, $m)) { + $data = json_decode($m[1], true); + } + } + + return $data ?: $raw; // nếu chưa parse được, trả raw để debug + } + private function getAllNetwork() { $check = $this->checkApiExits(); @@ -457,7 +593,7 @@ class vCenter try { Log::info('Bắt đầu clone VM với timeout 300 giây', ['template' => $template, 'target_name' => $body['spec']['name']]); - + $response = Http::withoutVerifying() ->timeout(300) // Tăng timeout lên 5 phút cho quá trình clone ->connectTimeout(30) // Giữ connect timeout ngắn để phát hiện lỗi kết nối sớm @@ -1854,8 +1990,11 @@ class vCenter return false; } } - - public function change_password_linux(array $data = ['ip' => '103.42.58.37', 'user' => 'root', 'password' => 'TGS@123', 'new_password' => '']): bool + /* + * ip = 103.42.58.37 + * pwd = TGS@123 + */ + public function change_password_linux(array $data = ['ip' => '10.10.10.173', 'user' => 'admin', 'password' => 'TGS@123', 'new_password' => '']): bool { try { // Kiểm tra mật khẩu mới @@ -2139,14 +2278,14 @@ class vCenter $dns = '8.8.8.8'; $vmware_name = ''; $vCenter = null; - + // Check if it's ModuleVmware (new structure) if ($product && isset($product->ipAddress) && $product->ipAddress) { Log::info('✅ Using ModuleVmware structure for assignIPWindows', [ 'module_vmware_id' => $product->id, 'vmware_id' => $vmware_id ]); - + // Extract from ModuleVmware->ipAddress relationship $ipAddress = $product->ipAddress->ip ?? null; $gateway = $product->ipAddress->gateway ?? null; @@ -2154,13 +2293,13 @@ class vCenter $dns = $product->ipAddress->dns ?? '8.8.8.8'; $vmware_name = $product->name ?? $vmware_id; $vCenter = $product->ipAddress->host->datacenter->vCenter ?? null; - + } elseif ($product && isset($product->IpAddress) && $product->IpAddress) { Log::info('✅ Using old Product structure for assignIPWindows', [ 'product_id' => $product->id, 'vmware_id' => $vmware_id ]); - + // Extract from old Product->IpAddress relationship $ipAddress = $product->IpAddress->ip ?? null; $gateway = $product->IpAddress->gateway ?? null; @@ -2169,7 +2308,7 @@ class vCenter $vmware_name = $product->name ?? $vmware_id; $vCenter = $product->IpAddress->Host->Datacenter->vCenter ?? null; } - + // Validate required data if (!$ipAddress || !$gateway || !$vCenter) { Log::error('❌ assignIPWindows: Missing required data', [ @@ -2186,10 +2325,10 @@ class vCenter } $this->SetConfig($vCenter); - + // Convert netmask to prefix length (e.g., 255.255.255.0 = 24) $maskBits = $this->convertNetmaskToPrefixLength($netmask); - + Log::channel('Custom')->info("==================================================================="); Log::channel('Custom')->info("Assign IP to Windows VM - $vmware_name"); Log::channel('Custom')->info("IP: $ipAddress, Gateway: $gateway, DNS: $dns, MaskBits: $maskBits"); @@ -2197,14 +2336,14 @@ class vCenter // Create PowerShell command with dynamic values $Command = "\$vmName = '$vmware_name'; \$IP = '$ipAddress'; \$MaskBits = $maskBits; \$Gateway = '$gateway'; \$DNS = '$dns'; cd /; C:\CLISave\AssignIP\assignIPWindows.ps1;"; - + Log::channel('Custom')->info("Executing Command: $Command"); - + $output = $this->Command($Command); - + Log::channel('Custom')->info("Script output: " . $output); Log::channel('Custom')->info("==================================================================="); - + // Check if execution was successful if (strpos($output, '🎉') !== false || strpos($output, 'successfully') !== false) { return [ @@ -2219,7 +2358,7 @@ class vCenter 'output' => $output ]; } - + } catch (\Exception $e) { Log::channel('Custom')->error("Exception in assignIPWindows: " . $e->getMessage()); return [ @@ -2261,7 +2400,7 @@ class vCenter '255.128.0.0' => 9, '255.0.0.0' => 8 ]; - + return $netmaskMap[$netmask] ?? 24; // Default to /24 if not found } diff --git a/app/Jobs/VirtualMachine/Action/ResetVmPasswordJob.php b/app/Jobs/VirtualMachine/Action/ResetVmPasswordJob.php new file mode 100644 index 0000000..e0d5ecd --- /dev/null +++ b/app/Jobs/VirtualMachine/Action/ResetVmPasswordJob.php @@ -0,0 +1,101 @@ +where('vmware_id', $this->productId) + ->where('customer_id', $this->userId) + ->firstOrFail(); + + $osFamily = (string)($product->os->osGroup->name ?? ''); + $isWindows = stripos($osFamily, 'windows') !== false; + if (!$isWindows) { + throw new \RuntimeException('Only Windows is supported by this reset job.'); + } + + $vmName = $product->ipAddress->vm_name + ?? $product->name + ?? $product->hostname + ?? null; + + if (!$vmName && preg_match('/^vm-\d+$/i', (string)$product->vmware_id)) { + $rawName = trim($serviceVCenter->runPsCommand("(Get-VM -Id '".addslashes($product->vmware_id)."').Name")); + if (($p = strpos($rawName, '[STDERR]')) !== false) { + $rawName = trim(substr($rawName, 0, $p)); + } + $vmName = trim(preg_replace('/\s+/', ' ', $rawName)); + } + if (!$vmName) { + throw new \RuntimeException('Cannot resolve VM Name on vCenter.'); + } + + $guestUser = 'Administrator'; + $guestPassword = $this->oldPassword; + + $result = $serviceVCenter->resetGuestPasswordViaPs1( + vmName: $vmName, + guestUser: $guestUser, + guestPassword: $guestPassword, + newPassword: $this->newPassword, + ); + + $product->update(['password' => encrypt($this->newPassword)]); + + if ($result === 'Login_Fail' || $result === false || $result === null) { + throw new \RuntimeException('SSH runner login failed.'); + } + + if (is_string($result)) { + throw new \RuntimeException('Script returned non-JSON output: '.$result); + } + + $row = $result; + if (isset($result[0])) { + $row = $result[0]; + foreach ($result as $it) { + if (is_array($it) && (($it['vm'] ?? null) === $vmName)) { $row = $it; break; } + } + } + + $status = strtolower($row['status'] ?? ''); + if ($status !== 'ok') { + throw new \RuntimeException($row['message'] ?? 'Reset failed by script'); + } + + // Gửi mail thông báo + Mail::to($this->email)->send( + new VmPasswordResetSuccess(vmName: $vmName, newPassword: $this->newPassword) + ); + + Log::info('ResetVmPasswordJob: success', ['vm'=>$vmName]); + } +} + diff --git a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php index 515a77d..995dbb8 100644 --- a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php +++ b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php @@ -104,16 +104,17 @@ class CreateVirtualMachineJob implements ShouldQueue 'ram' => 0, 'disk' => 0 ]; - + // Lấy addon từ order item if ($this->item->addons) { foreach ($this->item->addons as $orderAddon) { $addon = $orderAddon->addonItems; - if (!$addon) continue; - + if (!$addon) + continue; + $addonName = strtolower(trim($addon->name)); $quantity = (int) $orderAddon->qty; - + switch ($addonName) { case 'cpu': $addonData['cpu'] += $quantity; @@ -187,10 +188,10 @@ class CreateVirtualMachineJob implements ShouldQueue return false; } else { Log::info('Tạo máy ảo thành công: ' . json_encode($result)); - + // Lấy vm_id từ kết quả $vmId = is_array($result) ? ($result['data'] ?? 'unknown') : $result; - + if ($vmId !== 'unknown' && $vmId !== 'UNKNOWN') { try { // Lưu vm_id vào bảng invoice @@ -212,11 +213,11 @@ class CreateVirtualMachineJob implements ShouldQueue try { $customer = $this->item->order->customer; $serverName = $this->item->server_name ?? 'VM-' . $vmId; - + // 1. Lấy IP available từ IPPool $networkId = $product->ipAddress?->module_network_id ?? 1; $availableIP = $this->getAvailableIPFromPool($networkId); - + // 2. Tạo ModuleIpAddress với IP từ pool $moduleIpAddress = ModuleIpAddress::create([ 'vm_name' => $serverName, @@ -228,7 +229,7 @@ class CreateVirtualMachineJob implements ShouldQueue 'dns' => $product->ipAddress?->dns ?? '8.8.8.8', 'status' => 1 // 1 = Assigned ]); - + // 3. Tạo ModuleVmware với IP address vừa tạo $moduleVmware = ModuleVmware::create([ 'name' => $serverName, @@ -239,7 +240,7 @@ class CreateVirtualMachineJob implements ShouldQueue 'next_payment' => $this->item->order->next_payment ?? 0, 'next_due_date' => $this->item->order->next_due_date ?? null, 'next_invoice' => $this->item->order->next_invoice ?? 0, - 'user' => 'root', + 'user' => 'Administrator', 'password' => '', 'hostname' => $serverName, 'vmware_id' => $vmId, @@ -247,7 +248,7 @@ class CreateVirtualMachineJob implements ShouldQueue 'customer_id' => $customer->id, 'status' => 1 ]); - + Log::info('✅ Lưu dữ liệu VM và IP address thành công', [ 'vmware_id' => $vmId, 'module_vmware_id' => $moduleVmware->id, @@ -256,18 +257,18 @@ class CreateVirtualMachineJob implements ShouldQueue 'name' => $serverName, 'order_item_id' => $this->item->id ]); - + // Dispatch AssignIPJob để chạy sau 5 phút AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip) ->delay(now()->addMinutes(5)); - + Log::info('✅ Đã dispatch AssignIPJob để chạy sau 5 phút', [ 'vm_id' => $vmId, 'module_vmware_id' => $moduleVmware->id, 'assigned_ip' => $moduleIpAddress->ip, 'delay_minutes' => 5 ]); - + } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [ 'vmware_id' => $vmId, @@ -284,7 +285,7 @@ class CreateVirtualMachineJob implements ShouldQueue ]); } } - + // Gửi thông báo thành công $telegram->SendJobRunInfo( 'CreateVirtualMachineJob', @@ -333,22 +334,22 @@ class CreateVirtualMachineJob implements ShouldQueue $ipPoolList = IpPool::where('module_network_id', $networkId) ->orderBy('ip_address') ->get(); - + Log::info('✅ Lấy IP available từ pool', [ 'ip_pool_list' => $ipPoolList, 'network_id' => $networkId ]); - + if ($ipPoolList->isEmpty()) { // Nếu không có IP trong pool, trả về IP default return '0.0.0.0'; } - + // Lấy tất cả IP đã được assign trong ModuleIpAddress $assignedIPs = ModuleIpAddress::where('module_network_id', $networkId) ->pluck('ip') ->toArray(); - + // Tìm IP đầu tiên chưa được assign foreach ($ipPoolList as $ipPool) { if (!in_array($ipPool->ip_address, $assignedIPs)) { @@ -359,14 +360,14 @@ class CreateVirtualMachineJob implements ShouldQueue return $ipPool->ip_address; } } - + // Nếu tất cả IP đã được assign, log warning và trả về IP default Log::warning('⚠️ Không có IP available trong pool', [ 'network_id' => $networkId, 'total_ips_in_pool' => $ipPoolList->count(), 'assigned_ips' => count($assignedIPs) ]); - + return '0.0.0.0'; } @@ -381,21 +382,21 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$check) { return 'Login_Again'; } - + // Gọi API start VM trực tiếp $response = Http::withoutVerifying() ->withHeaders([ 'vmware-api-session-id' => $vCenter->token ]) ->post("https://{$vCenter->apiUri}/rest/vcenter/vm/{$vmId}/power/start"); - + if ($response->status() === 200) { return [ 'error' => false, 'data' => 'success' ]; } - + return [ 'error' => true, 'data' => $response->body() diff --git a/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php b/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php index 337cb1c..e660c9e 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php +++ b/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php @@ -29,10 +29,10 @@ class SyncAllPackage implements ShouldQueue */ public function handle() { - SyncDatacenter::dispatchSync(); - SyncHost::dispatchSync(); - SyncDatastore::dispatchSync(); - SyncNetwork::dispatchSync(); - SyncFolder::dispatchSync(); + SyncDatacenter::dispatch(); + SyncHost::dispatch(); + SyncDatastore::dispatch(); + SyncNetwork::dispatch(); + SyncFolder::dispatch(); } } diff --git a/app/Jobs/VirtualMachine/Sync/SyncNetwork.php b/app/Jobs/VirtualMachine/Sync/SyncNetwork.php index 9fc90ff..00b80a3 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncNetwork.php +++ b/app/Jobs/VirtualMachine/Sync/SyncNetwork.php @@ -86,7 +86,7 @@ class SyncNetwork implements ShouldQueue 'name' => $_subItem['name'], 'network' => $_subItem['network'], 'type' => $_subItem['type'], - 'ModuleHost_id' => $hostItem->id + 'module_host_id' => $hostItem->id ]); } diff --git a/app/Mail/VmPasswordResetSuccess.php b/app/Mail/VmPasswordResetSuccess.php new file mode 100644 index 0000000..e17562b --- /dev/null +++ b/app/Mail/VmPasswordResetSuccess.php @@ -0,0 +1,23 @@ +subject('Mật khẩu VPS của bạn đã được đặt lại') + ->view('emails.vm_password_reset_success'); + } +} diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php new file mode 100644 index 0000000..59599dc --- /dev/null +++ b/app/Providers/HorizonServiceProvider.php @@ -0,0 +1,36 @@ +email, [ + // + ]); + }); + } +} diff --git a/app/Services/Cache/CacheService.php b/app/Services/Cache/CacheService.php new file mode 100644 index 0000000..2b79407 --- /dev/null +++ b/app/Services/Cache/CacheService.php @@ -0,0 +1,57 @@ +ttl = $ttl; + } + + public function putCache(string $prefix, string $key, mixed $value, ?int $ttl = null): void + { + $cacheKey = $this->makeKey($prefix, $key); + Cache::put($cacheKey, $value, $ttl ?? $this->ttl); + } + + public function getCache(string $prefix, string $key, mixed $default = null): mixed + { + $cacheKey = $this->makeKey($prefix, $key); + return Cache::get($cacheKey, $default); + } + + public function clearCache(string $prefix, string $key): void + { + $cacheKey = $this->makeKey($prefix, $key); + Cache::forget($cacheKey); + } + + public function clearCacheByPrefix(string $prefix): void + { + + $cachePrefix = config('cache.prefix'); + + $pattern = $cachePrefix . $prefix . '_*'; + $keys = Redis::connection('cache')->keys($pattern); + + + foreach ($keys as $k) { + Redis::connection('cache')->del($k); + } + + } + + protected function makeKey(string $prefix, string $key = ''): string + { + return trim($prefix . '_' . $key, '_'); + } +} diff --git a/app/Services/Cache/CacheServiceInterface.php b/app/Services/Cache/CacheServiceInterface.php new file mode 100644 index 0000000..00ab45e --- /dev/null +++ b/app/Services/Cache/CacheServiceInterface.php @@ -0,0 +1,11 @@ + env('HORIZON_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('HORIZON_PATH', 'horizon'), + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ + + 'use' => 'default', + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ + + 'prefix' => env( + 'HORIZON_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + ), + + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ + + 'waits' => [ + 'redis:default' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ + + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], + + /* + |-------------------------------------------------------------------------- + | Silenced Jobs + |-------------------------------------------------------------------------- + | + | Silencing a job will instruct Horizon to not place the job in the list + | of completed jobs within the Horizon dashboard. This setting may be + | used to fully remove any noisy jobs from the completed jobs list. + | + */ + + 'silenced' => [ + // App\Jobs\ExampleJob::class, + ], + + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ + + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ + + 'fast_termination' => false, + + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon master + | supervisor may consume before it is terminated and restarted. For + | configuring these limits on your workers, see the next section. + | + */ + + 'memory_limit' => 64, + + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ + + 'defaults' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['default'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 1, + 'timeout' => 60, + 'nice' => 0, + ], + ], + + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'maxProcesses' => 10, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], + ], + + 'local' => [ + 'supervisor-1' => [ + 'maxProcesses' => 3, + ], + ], + ], +]; diff --git a/database/migrations/2025_08_03_223132_update_invoice_table.php b/database/migrations/2025_08_03_223132_update_invoice_table.php deleted file mode 100644 index 0d4adbf..0000000 --- a/database/migrations/2025_08_03_223132_update_invoice_table.php +++ /dev/null @@ -1,28 +0,0 @@ -unsignedInteger('rebuild_count')->nullable()->default(0)->after('vm_id'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('invoice', function (Blueprint $table) { - // - }); - } -}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a486387..eca5c49 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -29,5 +29,6 @@ class DatabaseSeeder extends Seeder $this->call(SnapshotLimitedSeeder::class); $this->call(TemplateSeeder::class); $this->call(Setting::class); + $this->call(MenuSeeder::class); } } diff --git a/resources/views/admin/form/vps.blade.php b/resources/views/admin/form/vps.blade.php index dec9f89..7a7a40d 100644 --- a/resources/views/admin/form/vps.blade.php +++ b/resources/views/admin/form/vps.blade.php @@ -174,5 +174,3 @@ - - diff --git a/resources/views/emails/vm_password_reset_success.blade.php b/resources/views/emails/vm_password_reset_success.blade.php new file mode 100644 index 0000000..cd85c9c --- /dev/null +++ b/resources/views/emails/vm_password_reset_success.blade.php @@ -0,0 +1,5 @@ +

Xin chào,

+

Mật khẩu VPS {{ $vmName }} đã được đặt lại thành công.

+

Mật khẩu mới: {{ $newPassword }}

+

Vui lòng đăng nhập và đổi lại mật khẩu theo ý bạn.

+

Trân trọng,

\ No newline at end of file diff --git a/resources/views/frontend/service/partials/configuration-info.blade.php b/resources/views/frontend/service/partials/configuration-info.blade.php index 6e438d3..2445539 100644 --- a/resources/views/frontend/service/partials/configuration-info.blade.php +++ b/resources/views/frontend/service/partials/configuration-info.blade.php @@ -41,7 +41,7 @@ Password
+ x-text="passwordVisible ? '' : '••••••••'">
-
+ @csrf + + {{ __('Mật khẩu cũ') }} + + {{ __('Mật khẩu mới') }} - - {{ $isWindows ? __('Tài khoản: administrator') : __('Tài khoản: root') }} + + {{ $isWindows ? __('Tài khoản: administrator') : __('Tài khoản: root') }} +
{{ __('Hủy') }} - {{ __('Đồng ý') }} + + {{ __('Đồng ý') }}
- - diff --git a/resources/views/frontend/service/partials/vm-control-panel.blade.php b/resources/views/frontend/service/partials/vm-control-panel.blade.php index 78c3992..40b205c 100644 --- a/resources/views/frontend/service/partials/vm-control-panel.blade.php +++ b/resources/views/frontend/service/partials/vm-control-panel.blade.php @@ -45,8 +45,7 @@ @include('frontend.service.partials.control-buttons.snapshot-button') @include('frontend.service.partials.control-buttons.reset-password-button') @include('frontend.service.partials.control-buttons.change-ip-button') - @endif - + @endif {{-- Notes Section --}} @@ -62,7 +61,7 @@ 'isWindows' => $isWindows, ]) @include('frontend.service.partials.change-ip-modal', ['invoice' => $invoice]) - @include('frontend.service.partials.change-remote-port-modal', ['invoice' => $invoice]) + @include('frontend.service.partials.change-remote-port-modal', ['invoice' => $invoice]) @include('frontend.service.partials.renewal-modal', ['invoice' => $invoice]) @endif