From 1384a2ec1cbd71dcd6b0eacb1cee54c9b21df58d Mon Sep 17 00:00:00 2001 From: pbaodev Date: Mon, 25 Aug 2025 01:45:38 +0700 Subject: [PATCH] update port remote change --- .../frontend/ServiceController.php | 114 +++++--------- app/Http/Services/vCenter.php | 124 +++++++++++++++ .../Action/ChangeRemotePortJob.php | 141 ++++++++++++++++++ app/Mail/ChangePortRemote.php | 26 ++++ app/Models/ModuleVmware.php | 4 +- .../views/emails/vm_port_change.blade.php | 5 + .../change-remote-port-modal.blade.php | 19 ++- 7 files changed, 349 insertions(+), 84 deletions(-) create mode 100644 app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php create mode 100644 app/Mail/ChangePortRemote.php create mode 100644 resources/views/emails/vm_port_change.blade.php diff --git a/app/Http/Controllers/frontend/ServiceController.php b/app/Http/Controllers/frontend/ServiceController.php index 7475a83..3591035 100644 --- a/app/Http/Controllers/frontend/ServiceController.php +++ b/app/Http/Controllers/frontend/ServiceController.php @@ -9,6 +9,7 @@ use App\Models\Invoice; use App\Models\InvoiceItem; use App\Repositories\Invoice\InvoiceRepositoryInterface; use App\Http\Services\vCenter; +use App\Jobs\VirtualMachine\Action\ChangeRemotePortJob; use App\Models\ModuleSnapshot; use App\Models\ModuleVmware; use App\Models\Order; @@ -600,85 +601,44 @@ class ServiceController extends Controller /** * Change Remote Desktop port (Windows only) */ - public function changeRemotePort(Request $request, vCenter $serviceVCenter) + public function changeRemotePort(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', - 'port' => 'required|integer|min:1|max:65535', - ]); - - 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); - $newPort = (int) $request->port; - - $product = ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'os.osGroup') - ->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); - } - - $osFamily = strtoupper($product->os->osGroup->name ?? ''); - if (!str_contains($osFamily, 'WINDOWS')) { - return response()->json([ - 'error' => true, - 'message' => __('Chỉ hỗ trợ Windows'), - ], 400); - } - - $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); - } - - if (!$serviceVCenter->loginServer($vcenter)) { - return response()->json([ - 'error' => true, - 'message' => __('Không thể kết nối đến vCenter'), - ], 500); - } - - // NOTE: Implement the actual change RDP port over WinRM/PowerShell here. - // Placeholder success until service method is available. - - return response()->json([ - 'error' => false, - 'message' => __('Yêu cầu đổi port RDP đã được ghi nhận'), - ]); - } catch (\Exception $e) { - Log::error('Lỗi changeRemotePort', [ - 'product_id' => $request->product_id ?? null, - 'error' => $e->getMessage(), - ]); - return response()->json([ - 'error' => true, - 'message' => __('Có lỗi xảy ra khi đổi port'), - ], 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_port' => 'required|string|min:4|max:128', + '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); + $newPort = (string) $request->new_port; + + // 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 + ChangeRemotePortJob::dispatch( + productId: $productId, + newPort: $newPort, + password: (string) $request->password, + 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 thay đổi cổng Remote Desktop. Quá trình sẽ mất 3-5 phút, vui lòng chờ đợi.'); } /** diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index 139a0b3..ba40b8b 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -425,6 +425,76 @@ PS; return "Error: " . $e->getMessage(); } } + + protected function CommandChangePort(string $cmd, bool $expectJson = false) + { + $auth = $this->LoginAuth(); + if (!$auth) + return "Login_Fail"; + + $esc = fn(string $v) => str_replace("'", "''", $v); + + $server = $esc($this->apiUri ?? ''); + $user = $esc($this->username ?? ''); + $pass = $esc($this->password ?? ''); + + // PowerShell script template + $psTemplate = <<<'PS' +$ProgressPreference = 'SilentlyContinue' +$InformationPreference = 'SilentlyContinue' +$WarningPreference = 'SilentlyContinue' +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Import-Module VMware.PowerCLI -ErrorAction Stop +Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null +Connect-VIServer -Server '' -User '' -Password '' | Out-Null + +& { + +} +PS; + + $psScript = strtr($psTemplate, [ + '' => $server, + '' => $user, + '' => $pass, + '' => $cmd, + ]); + + // Encode UTF-16LE cho -EncodedCommand + $encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8')); + $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) ?: ''; + + if ($expectJson) { + // CẮT JSON cuối: {...} hoặc [...] + $trimmed = trim($out); + // đơn giản: bắt khối { ... } hoặc [ ... ] ở cuối chuỗi + if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $trimmed, $m)) { + return $m[1]; + } + // không tìm thấy → trả nguyên STDOUT (để caller log raw) + return $trimmed; + } + + // Trường hợp không kỳ vọng JSON: giữ nguyên hành vi cũ (append STDERR) + 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 @@ -461,6 +531,60 @@ PS; return $data ?: $raw; // nếu chưa parse được, trả raw để debug } + + public function changeRDPPortWindows(string $vmName, string $guestUser, string $guestPassword, string $newPort): array|string + { + $psFile = "C:\\CLISave\\AssignIP\\change-rdp-port.ps1"; + $q = fn(string $v) => str_replace("'", "''", $v); + + // Dùng -VMName (string). Alias -VMNames cũng ok, nhưng mình set đúng tên tham số. + $args = " -VMName '" . $q($vmName) . "'"; + $args .= " -GuestUser '" . $q($guestUser) . "'"; + $args .= " -GuestPassword '" . $q($guestPassword) . "'"; // khớp tên tham số trong script 1-VM + $args .= " -NewPort " . (int) $newPort; + + $cmd = "& '$psFile'$args"; + + // Nếu đã có phiên bản CommandRSP($cmd, bool $expectJson) → bật expectJson để không trộn STDERR + $raw = method_exists($this, 'CommandRSP') + ? ($this->CommandChangePort($cmd, true) ?? '') + : ''; + + if ($raw === 'Login_Fail' || $raw === false || $raw === null) { + return "Login_Fail"; + } + $raw = trim($raw); + + // CẮT LẤY KHỐI JSON CUỐI (object hoặc array) nếu có noise + if (!preg_match('~(\{(?:[^{}]|(?1))*\}|\[(?:[^\[\]]|(?0))*\])\s*$~s', $raw, $m)) { + // fallback đơn giản hơn + if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $raw, $m)) { + $raw = $m[1]; + } + } else { + $raw = $m[1]; + } + + $data = json_decode($raw, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return [ + 'success' => false, + 'error' => 'JSON parsing failed', + 'raw_output' => $raw, + 'json_error' => json_last_error_msg() + ]; + } + + // Script 1-VM trả về 1 object, nhưng nếu lỡ trả mảng thì lấy phần tử đầu + if (isset($data[0])) { + $data = $data[0]; + } + + return $data; + } + + + private function getAllNetwork() { $check = $this->checkApiExits(); diff --git a/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php b/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php new file mode 100644 index 0000000..1398d90 --- /dev/null +++ b/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php @@ -0,0 +1,141 @@ +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'; + + $result = $serviceVCenter->changeRDPPortWindows( + vmName: $vmName, + guestUser: 'Administrator', + guestPassword: $this->password, + newPort: $this->newPort, + ); + + if ($result === 'Login_Fail' || $result === false || $result === null) { + throw new \RuntimeException('SSH runner login failed.'); + } + if (is_string($result)) { + // Script trả non-JSON → bắn lỗi kèm raw để debug + throw new \RuntimeException('Script returned non-JSON output: ' . $result); + } + + /** + * Chuẩn hoá $row: + * - Script 1-VM: trả 1 object: ['vm'=>.., 'status'=>.., 'message'=>.., 'newPort'=>..] + * - (Phòng hờ) Nếu lỡ trả array nhiều item, chọn item có 'vm' trùng $vmName, không thì lấy phần tử đầu. + */ + $row = null; + + if (is_array($result)) { + // case: object đơn + if (isset($result['vm']) && isset($result['status'])) { + $row = $result; + } else { + // case: mảng các object + // Tìm theo vmName + foreach ($result as $it) { + if (is_array($it) && (($it['vm'] ?? null) === $vmName)) { + $row = $it; + break; + } + } + // fallback: phần tử đầu tiên nếu chưa có + if ($row === null && isset($result[0]) && is_array($result[0])) { + $row = $result[0]; + } + } + } + + if (!is_array($row) || empty($row)) { + Log::error('ChangeRemotePortJob: invalid script response shape', ['vm' => $vmName, 'raw' => $result]); + throw new \RuntimeException('Invalid script response.'); + } + + // Đọc trường returned + $status = strtolower((string) ($row['status'] ?? '')); + $message = (string) ($row['message'] ?? ''); + $newPort = (string) ($row['newPort'] ?? $this->newPort); + + // Xét thành công: status === 'ok' + $ok = ($status === 'ok'); + + Log::info('ChangeRemotePortJob: script result', [ + 'vm' => $vmName, + 'status' => $status, + 'message' => $message, + 'newPort' => $newPort, + 'raw' => $row, + ]); + + if (!$ok) { + // Nếu muốn cho phép 'warning' pass, có thể đổi điều kiện ở trên hoặc thêm nhánh dưới: + // if ($status === 'warning' && str_contains(strtolower($message), 'rdp port is now')) { $ok = true; } + throw new \RuntimeException($message !== '' ? $message : ('Change RDP port failed with status: ' . $status)); + } + + // Gửi mail thông báo + Mail::to($this->email)->send( + new ChangePortRemote(vmName: $vmName, newPort: (string) $newPort) + ); + + Log::info('ChangeRemotePortJob: success', ['vm' => $vmName, 'newPort' => $newPort]); + } +} diff --git a/app/Mail/ChangePortRemote.php b/app/Mail/ChangePortRemote.php new file mode 100644 index 0000000..b46c200 --- /dev/null +++ b/app/Mail/ChangePortRemote.php @@ -0,0 +1,26 @@ +subject('Port VPS của bạn đã được thay đổi') + ->view('emails.vm_port_change'); + } +} diff --git a/app/Models/ModuleVmware.php b/app/Models/ModuleVmware.php index c280df3..ef53276 100644 --- a/app/Models/ModuleVmware.php +++ b/app/Models/ModuleVmware.php @@ -28,13 +28,15 @@ class ModuleVmware extends Model 'customer_id', 'status' ]; + + protected $casts = ['password' => 'hashed']; public function customer() { return $this->belongsTo(\App\Models\Customer::class); } public function ipAddress() { - return $this->belongsTo(\App\Models\ModuleIpAddress::class,'module_ip_address_id','id'); + return $this->belongsTo(\App\Models\ModuleIpAddress::class, 'module_ip_address_id', 'id'); } public function os() { diff --git a/resources/views/emails/vm_port_change.blade.php b/resources/views/emails/vm_port_change.blade.php new file mode 100644 index 0000000..e8a0995 --- /dev/null +++ b/resources/views/emails/vm_port_change.blade.php @@ -0,0 +1,5 @@ +

Xin chào,

+

Remote Desktop VPS {{ $vmName }} đã được đặt lại thành công.

+

Port mới: {{ $newPort }}

+

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/change-remote-port-modal.blade.php b/resources/views/frontend/service/partials/change-remote-port-modal.blade.php index a1471f6..3bfe08c 100644 --- a/resources/views/frontend/service/partials/change-remote-port-modal.blade.php +++ b/resources/views/frontend/service/partials/change-remote-port-modal.blade.php @@ -7,24 +7,31 @@ {{ __('Nhập port RDP mới cho máy ảo Windows (mặc định 3389).') }} -
+ @csrf {{ __('Port mới') }} - - {{ __('Chỉ áp dụng cho Windows. Kết nối đến sẽ cần dùng port này.') }} + + {{ __('Chỉ áp dụng cho Windows. Kết nối đến sẽ cần dùng port này.') }} + + + + {{ __('Mật khẩu') }} + + Vì lý do bảo mật, vui lòng xác thực mật khẩu VPS +
{{ __('Hủy') }} - {{ __('Đồng ý') }} + + {{ __('Đồng ý') }}
- -