diff --git a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php new file mode 100644 index 0000000..4e68822 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php @@ -0,0 +1,251 @@ +get('per_page', 15), 500); + $filter = $request->get('filter'); + $networkId = $request->get('network_id'); + + $query = IpPool::with('network:id,name,type,network'); + + if ($filter) { + $query->where('ip_address', 'like', "%{$filter}%"); + } + + if ($networkId) { + $query->where('module_network_id', $networkId); + } + + $paginator = $query->paginate($perPage); + + // Enrich each item with usage info + $usedIps = ModuleIpAddress::whereIn('ip', $paginator->pluck('ip_address')->toArray()) + ->where('status', 1) + ->with('host:id,name') + ->select(['id', 'ip', 'vm_name', 'module_host_id', 'module_network_id', 'status']) + ->get() + ->keyBy('ip'); + + // Get vmware info for used IPs + $vmwareMap = []; + if ($usedIps->isNotEmpty()) { + $vmwares = DB::table('module_vmware as mv') + ->join('module_ip_address as mia', 'mv.module_ip_address', '=', 'mia.id') + ->whereIn('mia.ip', $usedIps->keys()->toArray()) + ->leftJoin('services as s', 's.id', '=', 'mv.service_id') + ->leftJoin('customers as c', 'c.id', '=', 'mv.customer_id') + ->select([ + 'mia.ip', + 'mv.name as vm_name', + 'mv.id as vm_id', + 's.id as service_id', + 's.status as service_status', + 'c.fullname', + 'c.email', + ]) + ->get(); + + foreach ($vmwares as $vm) { + $vmwareMap[$vm->ip] = $vm; + } + } + + $items = $paginator->map(function (IpPool $pool) use ($usedIps, $vmwareMap) { + $data = $pool->toArray(); + $ip = $pool->ip_address; + + if (isset($usedIps[$ip])) { + $mia = $usedIps[$ip]; + $vm = $vmwareMap[$ip] ?? null; + $data['usage'] = [ + 'assigned' => true, + 'vm_name' => $vm?->vm_name ?? $mia->vm_name, + 'vm_id' => $vm?->vm_id ?? null, + 'service_id' => $vm?->service_id ?? null, + 'service_status' => $vm?->service_status ?? null, + 'customer_name' => $vm?->fullname ?? null, + 'customer_email' => $vm?->email ?? null, + ]; + } else { + $data['usage'] = ['assigned' => false]; + } + + return $data; + }); + + return response()->json([ + 'error' => false, + 'data' => [ + 'items' => $items->values(), + 'total' => $paginator->total(), + 'per_page' => $paginator->perPage(), + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + ], + ]); + } + + public function show(int $id): JsonResponse + { + $pool = IpPool::with('network:id,name,type,network')->find($id); + + if (!$pool) { + return response()->json(['error' => true, 'message' => "Không tìm thấy IP Pool #{$id}"], 404); + } + + return response()->json(['error' => false, 'data' => $pool]); + } + + /** + * Bulk create: ip_address may be comma-separated (e.g. "10.0.0.1,10.0.0.2"). + */ + public function store(Request $request): JsonResponse + { + $request->validate([ + 'ip_address' => 'required|string', + 'module_network_id' => 'required|integer|exists:module_network,id', + ]); + + $networkId = (int) $request->module_network_id; + $rawIps = $request->ip_address; + $ips = array_filter(array_map('trim', explode(',', $rawIps))); + + if (empty($ips)) { + return response()->json(['error' => true, 'message' => 'Không có địa chỉ IP hợp lệ'], 422); + } + + // Validate each IP + $invalid = []; + foreach ($ips as $ip) { + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $invalid[] = $ip; + } + } + + if (!empty($invalid)) { + return response()->json([ + 'error' => true, + 'message' => 'Địa chỉ IP không hợp lệ: ' . implode(', ', $invalid), + ], 422); + } + + // Check duplicates already in pool + $existing = IpPool::where('module_network_id', $networkId) + ->whereIn('ip_address', $ips) + ->pluck('ip_address') + ->toArray(); + + if (!empty($existing)) { + return response()->json([ + 'error' => true, + 'message' => 'Địa chỉ IP đã tồn tại trong pool: ' . implode(', ', $existing), + ], 422); + } + + DB::beginTransaction(); + try { + $created = []; + foreach ($ips as $ip) { + $created[] = IpPool::create([ + 'ip_address' => $ip, + 'module_network_id' => $networkId, + ]); + } + DB::commit(); + + return response()->json(['error' => false, 'data' => $created, 'count' => count($created)], 201); + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['error' => true, 'message' => $e->getMessage()], 500); + } + } + + public function update(int $id, Request $request): JsonResponse + { + $pool = IpPool::find($id); + + if (!$pool) { + return response()->json(['error' => true, 'message' => "Không tìm thấy IP Pool #{$id}"], 404); + } + + $request->validate([ + 'ip_address' => 'required|string|max:45', + 'module_network_id' => 'required|integer|exists:module_network,id', + ]); + + if (!filter_var($request->ip_address, FILTER_VALIDATE_IP)) { + return response()->json(['error' => true, 'message' => 'Địa chỉ IP không hợp lệ'], 422); + } + + // Check duplicate (excluding self) + $duplicate = IpPool::where('module_network_id', $request->module_network_id) + ->where('ip_address', $request->ip_address) + ->where('id', '!=', $id) + ->exists(); + + if ($duplicate) { + return response()->json([ + 'error' => true, + 'message' => 'Địa chỉ IP đã tồn tại trong pool này', + ], 422); + } + + DB::beginTransaction(); + try { + $pool->update($request->only(['ip_address', 'module_network_id'])); + DB::commit(); + + return response()->json(['error' => false, 'data' => $pool->fresh('network')]); + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['error' => true, 'message' => $e->getMessage()], 500); + } + } + + public function destroy(int $id): JsonResponse + { + $pool = IpPool::find($id); + + if (!$pool) { + return response()->json(['error' => true, 'message' => "Không tìm thấy IP Pool #{$id}"], 404); + } + + // Check if IP is currently assigned + $inUse = ModuleIpAddress::where('ip', $pool->ip_address) + ->where('module_network_id', $pool->module_network_id) + ->where('status', 1) + ->exists(); + + if ($inUse) { + return response()->json([ + 'error' => true, + 'message' => 'Không thể xoá — IP này đang được gán cho một máy chủ', + ], 422); + } + + $pool->delete(); + + return response()->json(['error' => false, 'message' => 'Đã xoá IP Pool thành công']); + } + + public function networks(): JsonResponse + { + $networks = ModuleNetwork::select(['id', 'name', 'type', 'network']) + ->orderBy('name') + ->get(); + + return response()->json(['error' => false, 'data' => $networks]); + } +} diff --git a/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php b/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php index a878835..0b0e6f9 100644 --- a/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php +++ b/app/Jobs/VirtualMachine/Provision/SendVmCredentialsEmailJob.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Jobs\VirtualMachine\Provision; +use App\Jobs\Mail\Admin\AdminOrderNotificationJob; use App\Jobs\Mail\SendInformationVirtualMachine; use App\Models\Invoice; use App\Models\ModuleVmware; @@ -112,6 +113,27 @@ class SendVmCredentialsEmailJob implements ShouldQueue 'ip' => $vm->ip, 'invoice_attached' => $invoicePdf !== null, ]); + + // Notify admin with VM credentials after successful provisioning + $order = $vm->order; + AdminOrderNotificationJob::dispatch([ + 'customer_name' => trim($customer->firstname . ' ' . $customer->lastname), + 'customer_email' => $customer->email, + 'client_ip' => null, + 'order_num' => $order?->order_num ?? $order?->id ?? 'N/A', + 'product_name' => $orderItem?->getProductAttribute()?->name ?? 'Cloud VPS', + 'product_type' => 'Cloud VPS', + 'billing_cycle' => $this->billingLabel($months), + '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 ?? '', + 'password' => $vm->plain_password ?? '', + ]); + + Log::info('SendVmCredentialsEmailJob: admin notification dispatched', [ + 'module_vmware_id' => $this->moduleVmwareId, + ]); } public function failed(\Throwable $e): void diff --git a/routes/api.php b/routes/api.php index 43f800b..44aac9d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,6 +20,7 @@ use App\Http\Controllers\Api\Admin\DnsManager\ServerController as DnsServerContr use App\Http\Controllers\Api\Admin\DnsManager\DomainController as DnsDomainController; use App\Http\Controllers\Api\Admin\OsManager\OsGroupController; use App\Http\Controllers\Api\Admin\OsManager\OsController; +use App\Http\Controllers\Api\Admin\IpPool\IpPoolController; use App\Http\Controllers\Api\System\CompressionTestController; use App\Http\Services\Media; use App\Http\Services\vCenter as ServiceVCenter; @@ -311,6 +312,18 @@ Route::prefix('v1') Route::get('jobs/metrics', 'metrics'); }); + // IP Pool (module: module-vcenter) + Route::prefix('ip-pool') + ->controller(IpPoolController::class) + ->group(function () { + 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::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'); + }); + // vCenter Manager Power Operations (module: module-vcenter) Route::prefix('vcenter-manager') ->controller(VCenterManagerController::class)