diff --git a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php index 4e68822..1376b2a 100644 --- a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php +++ b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php @@ -240,6 +240,42 @@ class IpPoolController extends Controller return response()->json(['error' => false, 'message' => 'Đã xoá IP Pool thành công']); } + public function bulkDelete(Request $request): JsonResponse + { + $request->validate([ + 'ids' => 'required|array|min:1', + 'ids.*' => 'integer', + ]); + + $ids = $request->ids; + $pools = IpPool::whereIn('id', $ids)->get(); + + if ($pools->isEmpty()) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy IP Pool nào'], 404); + } + + // Block deletion if any IP is currently assigned + $inUseIps = ModuleIpAddress::whereIn('ip', $pools->pluck('ip_address')) + ->where('status', 1) + ->pluck('ip') + ->toArray(); + + if (!empty($inUseIps)) { + return response()->json([ + 'error' => true, + 'message' => 'Không thể xoá — các IP sau đang được gán cho máy chủ: ' . implode(', ', $inUseIps), + ], 422); + } + + $deleted = IpPool::whereIn('id', $ids)->delete(); + + return response()->json([ + 'error' => false, + 'message' => "Đã xoá {$deleted} IP Pool thành công", + 'deleted' => $deleted, + ]); + } + public function networks(): JsonResponse { $networks = ModuleNetwork::select(['id', 'name', 'type', 'network']) diff --git a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php index 68f91c0..dd13d6c 100644 --- a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php +++ b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php @@ -9,6 +9,7 @@ use App\Models\IpPool; use App\Models\ModuleIpAddress; use App\Models\ModuleVmware; use App\Models\Os; +use App\Http\Services\vCenter as vCenterService; use App\Models\vCenter; use App\Services\VsphereHostSshService; use Illuminate\Bus\Queueable; @@ -29,13 +30,12 @@ use Illuminate\Support\Facades\Log; * [1] Resolve network config from order item → product → ipAddress template * [2] for attempt 1..MAX_ATTEMPTS: * [3] Pick a free IP with DB transaction + lock (reserveIp) - * [4] Pre-check: ping IP — if reachable → release and try next - * [5] Call assignIPLinux.ps1 / assignIPWindows.ps1 via SSH - * [6] On FAIL → release IP, try next - * [7] Call verifyIPLinux.ps1 / verifyIPWindows.ps1 via SSH - * [8] On FAIL → release IP, try next - * [9] SUCCESS → persistIp() → dispatch SendVmCredentialsEmailJob - * [10] All attempts exhausted → mark VM inactive (status=0) + * [4] Call assignIPLinux.ps1 / assignIPWindows.ps1 via SSH + * [5] On FAIL → release IP, try next + * [6] Call verifyIPLinux.ps1 / verifyIPWindows.ps1 via SSH + * [7] On FAIL → release IP, try next + * [8] SUCCESS → persistIp() → dispatch SendVmCredentialsEmailJob + * [9] All attempts exhausted → mark VM inactive (status=0) */ class AssignIpToGuestJob implements ShouldQueue { @@ -58,7 +58,7 @@ class AssignIpToGuestJob implements ShouldQueue // ───────────────────────────────────────────────────────────────────────── - public function handle(VsphereHostSshService $ssh, vCenter $vCenterService): void + public function handle(VsphereHostSshService $ssh, vCenterService $vCenterService): void { Log::info('AssignIpToGuestJob: start', [ 'module_vmware_id' => $this->moduleVmwareId, @@ -118,16 +118,6 @@ class AssignIpToGuestJob implements ShouldQueue $ip = $reservation->ip; - // [4] Pre-check: skip IPs that are already reachable (in use by another host) - if ($this->isIpReachable($ip)) { - Log::warning('AssignIpToGuestJob: IP reachable (likely in use), skipping', [ - 'ip' => $ip, - 'attempt' => $attempt, - ]); - $this->releaseIp($reservation); - continue; - } - // [5] Assign IP into VM guest OS via PS1 script $assignOutput = $ssh->assignIp( vmId: $this->vmId, @@ -339,16 +329,6 @@ class AssignIpToGuestJob implements ShouldQueue // Micro-helpers // ───────────────────────────────────────────────────────────────────────── - /** - * Ping the IP from the Laravel host (1 packet, 1 s timeout). - * Returns true if reachable → the IP is already in use by something else. - */ - private function isIpReachable(string $ip): bool - { - exec(sprintf('ping -c 1 -W 1 %s 2>/dev/null', escapeshellarg($ip)), $output, $exitCode); - return $exitCode === 0; - } - /** PS1 scripts output exactly "SUCCESS" or "FAIL". */ private function isSuccess(string $output): bool { diff --git a/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php b/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php index 0b0e6f9..fcd5891 100644 --- a/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php +++ b/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php @@ -64,19 +64,23 @@ class SendVmCredentialsEmailJob implements ShouldQueue $orderItem = $vm->order?->orderItems?->first(); $months = (int) ($orderItem?->months ?? 0); + $osGroupName = $vm->os?->osGroup?->name ?? ''; + $isWindows = str_contains(strtolower($osGroupName), 'windows'); + $osUsername = $isWindows ? 'Administrator' : 'root'; + $emailData = [ 'company_name' => $settingValue->company ?? 'Thế Giới Số', 'company_logo' => $settingValue->logo ?? '', - 'client_name' => trim($customer->firstname . ' ' . $customer->lastname), + 'client_name' => $customer->fullname ?? '', 'product_name' => $orderItem?->getProductAttribute()?->name ?? 'Cloud VPS', 'product_billing' => $this->billingLabel($months), 'product_price' => $orderItem ? number_format((float) $orderItem->price, 0, ',', '.') : 'N/A', 'service_host' => $vm->ip ?? 'N/A', - 'service_username' => $vm->user, + 'service_username' => $osUsername, 'service_port' => $vm->port, 'service_password' => $vm->plain_password ?? '(xem tài khoản quản lý)', 'os_name' => $vm->os - ? (($vm->os->osGroup?->name ?? '') . ' ' . $vm->os->version) + ? (($osGroupName . ' ' . $vm->os->version)) : 'Unknown OS', 'service_due_exp' => $vm->next_due_date ? date('d/m/Y', (int) $vm->next_due_date) @@ -117,7 +121,7 @@ class SendVmCredentialsEmailJob implements ShouldQueue // Notify admin with VM credentials after successful provisioning $order = $vm->order; AdminOrderNotificationJob::dispatch([ - 'customer_name' => trim($customer->firstname . ' ' . $customer->lastname), + 'customer_name' => $customer->fullname ?? '', 'customer_email' => $customer->email, 'client_ip' => null, 'order_num' => $order?->order_num ?? $order?->id ?? 'N/A', @@ -127,7 +131,7 @@ class SendVmCredentialsEmailJob implements ShouldQueue 'total' => $orderItem?->price ?? 0, 'created_at' => $order?->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), 'ip' => $vm->ip ?? '', - 'username' => $vm->user ?? '', + 'username' => $osUsername, 'password' => $vm->plain_password ?? '', ]); diff --git a/app/Models/IpPool.php b/app/Models/IpPool.php index 0bd1151..cb1ecbf 100644 --- a/app/Models/IpPool.php +++ b/app/Models/IpPool.php @@ -4,10 +4,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class IpPool extends Model { - use HasFactory; + use HasFactory, SoftDeletes; protected $fillable = [ 'ip_address', diff --git a/database/migrations/2026_05_13_000001_add_soft_deletes_to_ip_pools_table.php b/database/migrations/2026_05_13_000001_add_soft_deletes_to_ip_pools_table.php new file mode 100644 index 0000000..b87f4e5 --- /dev/null +++ b/database/migrations/2026_05_13_000001_add_soft_deletes_to_ip_pools_table.php @@ -0,0 +1,21 @@ +softDeletes(); + }); + } + + public function down(): void + { + Schema::table('ip_pools', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/resources/views/emails/admin/order-notification.blade.php b/resources/views/emails/admin/order-notification.blade.php index 5972feb..53e1f81 100644 --- a/resources/views/emails/admin/order-notification.blade.php +++ b/resources/views/emails/admin/order-notification.blade.php @@ -5,39 +5,46 @@ Thông báo đơn hàng mới
-
-

📦 Đơn hàng mới đã được thanh toán

- Thông báo tự động từ hệ thống quản lý -
+
+
+
📦
+

Đơn hàng mới đã thanh toán

+

Đơn #{{ $order_num ?? 'N/A' }} — {{ $created_at ?? now()->format('d/m/Y H:i:s') }}

+
-
-

Một đơn hàng mới vừa được khách hàng tạo và thanh toán thành công trên hệ thống.

- -
Thông tin khách hàng
-
- +
+ +
@@ -46,16 +53,16 @@ + @if(!empty($client_ip)) - + + @endif
Tên khách hàng {{ $customer_name ?? 'N/A' }}Email {{ $customer_email ?? 'N/A' }}
IP đặt hàng{{ $client_ip ?? 'N/A' }}{{ $client_ip }}
-
-
Thông tin đơn hàng
-
- + +
@@ -72,55 +79,45 @@ - + - + - - - - - +
Mã đơn hàng #{{ $order_num ?? 'N/A' }}Chu kỳ thanh toán {{ $billing_cycle ?? 'N/A' }}
Tổng tiền{{ number_format($total ?? 0, 0, ',', '.') }} VNĐ{{ number_format($total ?? 0, 0, ',', '.') }} VNĐ
Trạng tháiĐã thanh toán
Thời gian{{ $created_at ?? now()->format('d/m/Y H:i:s') }}✅ Đã thanh toán
-
- @if(!empty($username) || !empty($password) || !empty($ip)) -
Thông tin dịch vụ
-
- + @if(!empty($ip) || !empty($username) || !empty($password)) + +
+ @if(!empty($ip)) +
+ 🌐 IP Address + {{ $ip }} +
+ @endif @if(!empty($username)) -
- - - +
+ 👤 Username + {{ $username }} +
@endif @if(!empty($password)) - - - - +
+ 🔑 Password + {{ $password }} +
@endif - @if(!empty($ip)) - - - - - @endif -
Username{{ $username }}
Password{{ $password }}
IP Address{{ $ip }}
+
+ @endif
- @endif -
-

- Email này được gửi tự động bởi hệ thống. Vui lòng không trả lời email này. -

-
- -
diff --git a/resources/views/emails/customer/order-payment-success.blade.php b/resources/views/emails/customer/order-payment-success.blade.php new file mode 100644 index 0000000..6cb6495 --- /dev/null +++ b/resources/views/emails/customer/order-payment-success.blade.php @@ -0,0 +1,101 @@ + + + + + + Đặt hàng thành công + + + +
+
+
+
✅
+

Đặt hàng thành công!

+

Đơn hàng #{{ $orderNum ?? 'N/A' }} đã được xác nhận và thanh toán

+
+ +
+

+ Xin chào {{ $customerName ?? 'Quý khách' }},
+ Cảm ơn bạn đã tin tưởng sử dụng dịch vụ của chúng tôi. Đơn hàng của bạn đã được xác nhận và đang trong quá trình khởi tạo. +

+ + + + + +
Mã đơn hàng#{{ $orderNum ?? 'N/A' }}
Tổng thanh toán{{ number_format($amount ?? 0, 0, ',', '.') }} VNĐ
+ + @if(!empty($vmResults)) + + @foreach($vmResults as $vm) +
+ @if(!empty($vm['ip'])) +
+ 🌐 IP Address + {{ $vm['ip'] }} +
+ @endif + @if(!empty($vm['username'])) +
+ 👤 Username + {{ $vm['username'] }} +
+ @endif + @if(!empty($vm['password'])) +
+ 🔑 Password + {{ $vm['password'] }} +
+ @endif + @if(!empty($vm['os'])) +
+ 💿 Hệ điều hành + {{ $vm['os'] }} +
+ @endif +
+ @endforeach + @endif + +
+ ⚠️ Vui lòng đổi mật khẩu ngay sau khi đăng nhập lần đầu để bảo mật tài khoản.
+ Nếu cần hỗ trợ, liên hệ: {{ $supportEmail ?? '' }} hoặc {{ $supportPhone ?? '' }} +
+
+ + +
+
+ + diff --git a/routes/api.php b/routes/api.php index 44aac9d..cc6643b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -319,6 +319,7 @@ Route::prefix('v1') Route::get('networks', 'networks')->middleware('perm:module-vcenter.view'); Route::get('/', 'index')->middleware('perm:module-vcenter.view'); Route::post('/', 'store')->middleware('perm:module-vcenter.manager'); + Route::delete('bulk', 'bulkDelete')->middleware('perm:module-vcenter.manager'); Route::get('{id}', 'show')->middleware('perm:module-vcenter.view'); Route::put('{id}', 'update')->middleware('perm:module-vcenter.manager'); Route::delete('{id}', 'destroy')->middleware('perm:module-vcenter.manager');