From 2deb4ad74eaf9cfed7cde0781e7b3ff374efc56d Mon Sep 17 00:00:00 2001 From: baodev0 Date: Mon, 17 Aug 2026 11:55:48 +0700 Subject: [PATCH] feat: Enhance service management and transaction handling - Implemented client access token verification middleware to secure API endpoints. - Updated ServiceController to fetch active hosting services for authenticated users. - Refactored OrderController to ensure order retrieval is customer-specific. - Improved error logging in VmController for unexpected vCenter responses. - Disabled VNPay auto-deposit in TransactionController, switching to manual deposit flow. - Enhanced ServiceClientDetailResource to include Plesk credentials for hosting accounts. - Adjusted VpsServiceResource to dynamically set page type based on product type. - Added logging for PowerShellService to capture errors in RDP port changes. - Provisioning job now resolves service plans and logs relevant information for hosting accounts. - Updated database migrations to add necessary fields for service plans and DNS servers. - Refactored Service and ServicePlan models to establish relationships with hosting accounts and DNS servers. --- .claude/settings.local.json | 4 +- .env.example | 4 + .../Api/Admin/DnsManager/ServerController.php | 13 + .../Admin/Hosting/ServicePlanController.php | 60 ++-- .../Api/Client/Service/ServiceController.php | 24 +- .../Api/Service/OrderControllerRefactor.php | 5 +- .../Controllers/Api/Service/VmController.php | 12 +- .../Api/Transaction/TransactionController.php | 323 +++++++++--------- app/Http/Kernel.php | 1 + .../Middleware/ClientAccessTokenCheck.php | 50 +++ .../Service/ServiceClientDetailResource.php | 33 ++ .../Api/Service/VpsServiceResource.php | 2 +- .../Services/vCenter/PowerShellService.php | 8 +- .../Hosting/ProvisionHostingAccountJob.php | 54 ++- app/Models/DnsServer.php | 1 + app/Models/Service.php | 6 + app/Models/ServicePlan.php | 6 + .../Api/Order/OrderRepository.php | 9 + .../ServiceClient/ServiceClientRepository.php | 5 +- .../Api/Domain/DomainOrderService.php | 1 + app/Services/Api/Order/OrderService.php | 1 + app/Services/Plesk/PleskHostingService.php | 4 +- config/services.php | 6 + ...8_17_104858_add_type_to_services_table.php | 31 ++ ...nager_server_id_to_service_plans_table.php | 23 ++ ...anel_port_to_dns_manager_servers_table.php | 22 ++ routes/client.php | 3 +- yarn.lock | 7 +- 28 files changed, 489 insertions(+), 229 deletions(-) create mode 100644 app/Http/Middleware/ClientAccessTokenCheck.php create mode 100644 database/migrations/2026_08_17_104858_add_type_to_services_table.php create mode 100644 database/migrations/2026_08_17_105733_add_dns_manager_server_id_to_service_plans_table.php create mode 100644 database/migrations/2026_08_17_111510_add_panel_port_to_dns_manager_servers_table.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fe4f96e..363d0cb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -47,7 +47,9 @@ "Bash(node -e \"console.log\\(require.resolve\\('ag-grid-react/package.json'\\)\\)\")", "Bash(awk 'NR==1,NR==900' routes/api.php)", "Bash(sed -i '' -e 's/#c93131/#F16723/g' -e 's/#FFF6F6/#FFF1E6/g' -e 's/#ea4544/#F16723/g' -e 's/#EA4544/#F16723/g' -e 's/#e0cccc/#e6e6e6/g' portal/index.html)", - "Bash(sed -i '' -e 's/#c93131/#F16723/g' -e 's/#FFF6F6/#FFF1E6/g' -e 's/#ea4544/#F16723/g' -e 's/#EA4544/#F16723/g' -e 's/#e0cccc/#e6e6e6/g' index.html)" + "Bash(sed -i '' -e 's/#c93131/#F16723/g' -e 's/#FFF6F6/#FFF1E6/g' -e 's/#ea4544/#F16723/g' -e 's/#EA4544/#F16723/g' -e 's/#e0cccc/#e6e6e6/g' index.html)", + "Bash(xargs -I{} basename {})", + "Bash(uniq -c -w7)" ] } } diff --git a/.env.example b/.env.example index 4561fab..94cba16 100644 --- a/.env.example +++ b/.env.example @@ -83,6 +83,10 @@ VNPAY_API_URL=http://sandbox.vnpayment.vn/merchant_webapi/merchant.html VNPAY_TRANSACTION_API=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction FRONTEND_URL=https://portal.test +# openssl rand -hex 32 +CLIENT_API_ACCESS_KEY= +CLIENT_API_ACCESS_SECRET="tgs-client-api" + WMKS_HOST=10.10.10.120 \ No newline at end of file diff --git a/app/Http/Controllers/Api/Admin/DnsManager/ServerController.php b/app/Http/Controllers/Api/Admin/DnsManager/ServerController.php index 5e03fcd..17ff128 100644 --- a/app/Http/Controllers/Api/Admin/DnsManager/ServerController.php +++ b/app/Http/Controllers/Api/Admin/DnsManager/ServerController.php @@ -22,6 +22,7 @@ class ServerController extends Controller $perPage = min((int) $request->get('per_page', 15), 500); $filter = $request->get('filter'); $status = $request->get('status'); + $type = $request->get('type'); $query = $this->serverRepository->model->newQuery(); @@ -37,6 +38,14 @@ class ServerController extends Controller $query->where('status', $status); } + if ($type === 'hosting') { + $query->whereIn('type', ['hosting', 'both']); + } elseif ($type === 'domain') { + $query->whereIn('type', ['domain', 'both']); + } elseif ($type !== null && $type !== '') { + $query->where('type', $type); + } + $paginator = $query->orderByDesc('id')->paginate($perPage); return response()->json([ @@ -70,6 +79,7 @@ class ServerController extends Controller $validated = $request->validate([ 'name' => 'required|string|max:255', 'ip_address' => 'required|string|max:255', + 'panel_port' => 'nullable|integer|min:1|max:65535', 'username' => 'required|string|max:255', 'password' => 'required|string|max:255', 'api_key' => 'nullable|string', @@ -83,6 +93,7 @@ class ServerController extends Controller $server = $this->serverRepository->create([ 'name' => $validated['name'], 'ip_address' => $validated['ip_address'], + 'panel_port' => $validated['panel_port'] ?? 8880, 'username' => $validated['username'], 'password' => Crypt::encryptString($validated['password']), 'api_key' => $validated['api_key'] ?? null, @@ -114,6 +125,7 @@ class ServerController extends Controller $validated = $request->validate([ 'name' => 'sometimes|required|string|max:255', 'ip_address' => 'sometimes|required|string|max:255', + 'panel_port' => 'nullable|integer|min:1|max:65535', 'username' => 'sometimes|required|string|max:255', 'password' => 'nullable|string|max:255', 'api_key' => 'nullable|string', @@ -182,6 +194,7 @@ class ServerController extends Controller 'id' => $server->id, 'name' => $server->name, 'ip_address' => $server->ip_address, + 'panel_port' => $server->panel_port, 'username' => $server->username, 'api_key' => $server->api_key, 'status' => $server->status, diff --git a/app/Http/Controllers/Api/Admin/Hosting/ServicePlanController.php b/app/Http/Controllers/Api/Admin/Hosting/ServicePlanController.php index 456dabf..9329269 100644 --- a/app/Http/Controllers/Api/Admin/Hosting/ServicePlanController.php +++ b/app/Http/Controllers/Api/Admin/Hosting/ServicePlanController.php @@ -11,14 +11,14 @@ class ServicePlanController extends Controller { public function index(): JsonResponse { - $plans = ServicePlan::with('plesk')->orderBy('title')->get(); + $plans = ServicePlan::with('plesk', 'dnsServer')->orderBy('title')->get(); return response()->json(['error' => false, 'data' => $plans]); } public function show(int $id): JsonResponse { - $plan = ServicePlan::with('plesk')->find($id); + $plan = ServicePlan::with('plesk', 'dnsServer')->find($id); if (!$plan) { return response()->json(['error' => true, 'message' => 'Không tìm thấy gói hosting.'], 404); @@ -30,21 +30,23 @@ class ServicePlanController extends Controller public function store(Request $request): JsonResponse { $validated = $request->validate([ - 'title' => 'required|string|max:255', - 'vcpu' => 'required|integer|min:0', - 'ram' => 'required|integer|min:0', - 'disk' => 'required|integer|min:0', - 'domain' => 'required|integer|min:0', - 'plesk_plan_name' => 'nullable|string|max:255', - 'note' => 'nullable|string', + 'title' => 'required|string|max:255', + 'vcpu' => 'required|integer|min:0', + 'ram' => 'required|integer|min:0', + 'disk' => 'required|integer|min:0', + 'domain' => 'required|integer|min:0', + 'dns_manager_server_id' => 'nullable|integer|exists:dns_manager_servers,id', + 'plesk_plan_name' => 'nullable|string|max:255', + 'note' => 'nullable|string', ]); $plan = ServicePlan::create([ - 'title' => $validated['title'], - 'vcpu' => $validated['vcpu'], - 'ram' => $validated['ram'], - 'disk' => $validated['disk'], - 'domain' => $validated['domain'], + 'title' => $validated['title'], + 'vcpu' => $validated['vcpu'], + 'ram' => $validated['ram'], + 'disk' => $validated['disk'], + 'domain' => $validated['domain'], + 'dns_manager_server_id' => $validated['dns_manager_server_id'] ?? null, ]); $this->savePlesk($plan, $validated); @@ -52,7 +54,7 @@ class ServicePlanController extends Controller return response()->json([ 'error' => false, 'message' => 'Tạo gói hosting thành công.', - 'data' => $plan->load('plesk'), + 'data' => $plan->load('plesk', 'dnsServer'), ], 201); } @@ -65,21 +67,23 @@ class ServicePlanController extends Controller } $validated = $request->validate([ - 'title' => 'required|string|max:255', - 'vcpu' => 'required|integer|min:0', - 'ram' => 'required|integer|min:0', - 'disk' => 'required|integer|min:0', - 'domain' => 'required|integer|min:0', - 'plesk_plan_name' => 'nullable|string|max:255', - 'note' => 'nullable|string', + 'title' => 'required|string|max:255', + 'vcpu' => 'required|integer|min:0', + 'ram' => 'required|integer|min:0', + 'disk' => 'required|integer|min:0', + 'domain' => 'required|integer|min:0', + 'dns_manager_server_id' => 'nullable|integer|exists:dns_manager_servers,id', + 'plesk_plan_name' => 'nullable|string|max:255', + 'note' => 'nullable|string', ]); $plan->update([ - 'title' => $validated['title'], - 'vcpu' => $validated['vcpu'], - 'ram' => $validated['ram'], - 'disk' => $validated['disk'], - 'domain' => $validated['domain'], + 'title' => $validated['title'], + 'vcpu' => $validated['vcpu'], + 'ram' => $validated['ram'], + 'disk' => $validated['disk'], + 'domain' => $validated['domain'], + 'dns_manager_server_id' => $validated['dns_manager_server_id'] ?? null, ]); $this->savePlesk($plan, $validated); @@ -87,7 +91,7 @@ class ServicePlanController extends Controller return response()->json([ 'error' => false, 'message' => 'Cập nhật gói hosting thành công.', - 'data' => $plan->fresh('plesk'), + 'data' => $plan->fresh(['plesk', 'dnsServer']), ]); } diff --git a/app/Http/Controllers/Api/Client/Service/ServiceController.php b/app/Http/Controllers/Api/Client/Service/ServiceController.php index 9088098..a5eaf7d 100644 --- a/app/Http/Controllers/Api/Client/Service/ServiceController.php +++ b/app/Http/Controllers/Api/Client/Service/ServiceController.php @@ -53,21 +53,17 @@ class ServiceController extends Controller public function indexHosting(Request $request): JsonResponse { $perPage = min((int) $request->get('per_page', 15), 100); + $customer = $request->user(); - return response()->json([ - 'error' => false, - 'message' => 'Lấy dữ liệu thành công', - 'page_type' => 'service_hosting', - 'data' => [ - 'items' => [], - 'pagination' => [ - 'total' => 0, - 'per_page' => $perPage, - 'current_page' => 1, - 'last_page' => 1, - ], - ], - ]); + $services = $this->serviceClientService->getActiveServices($customer->id, 'hosting', $perPage); + + return (new VpsServiceCollection($services)) + ->additional([ + 'error' => false, + 'message' => 'Lấy dữ liệu thành công', + 'page_type' => 'service_hosting', + ]) + ->response(); } public function indexWebDesign(Request $request): JsonResponse diff --git a/app/Http/Controllers/Api/Service/OrderControllerRefactor.php b/app/Http/Controllers/Api/Service/OrderControllerRefactor.php index 0cca618..ab7f47b 100644 --- a/app/Http/Controllers/Api/Service/OrderControllerRefactor.php +++ b/app/Http/Controllers/Api/Service/OrderControllerRefactor.php @@ -128,13 +128,14 @@ class OrderControllerRefactor extends Controller public function retryOrderProvisioning(Request $request, int $id): JsonResponse { - $order = $this->orderRepository->find($id); + $customerId = (int) $request->user()->id; + $order = $this->orderRepository->findForCustomer($id, $customerId); if (! $order) { return response()->json(['success' => false, 'message' => 'Đơn hàng không tồn tại.'], 404); } - $result = $this->orderQueryService->retryProvisioning($order, (int) $request->user()->id); + $result = $this->orderQueryService->retryProvisioning($order, $customerId); if (! $result['ok']) { return response()->json(['success' => false, 'message' => $result['message']], $result['code']); diff --git a/app/Http/Controllers/Api/Service/VmController.php b/app/Http/Controllers/Api/Service/VmController.php index 4c3c8e2..dce35d1 100644 --- a/app/Http/Controllers/Api/Service/VmController.php +++ b/app/Http/Controllers/Api/Service/VmController.php @@ -262,10 +262,14 @@ class VmController extends Controller ], $ok ? 200 : 400); } + \Illuminate\Support\Facades\Log::error('changePort: unexpected vCenter response', [ + 'vm_id' => $vm->id, + 'result' => $result, + ]); + return response()->json([ 'success' => false, 'message' => 'Phản hồi không hợp lệ từ vCenter.', - 'data' => ['raw' => $result], ], 400); } @@ -280,9 +284,9 @@ class VmController extends Controller return response()->json(['error' => true, 'message' => 'Please choose product']); } - $vm = ModuleVmware::with('vCenter')->find((int) $productId); - if (!$vm) { - return response()->json(['error' => true, 'message' => "Can't find product"]); + $vm = $this->resolveVmById($productId, $request); + if ($vm instanceof JsonResponse) { + return $vm; } $result = $this->getConsoleTicket($serviceVCenter, $vm); diff --git a/app/Http/Controllers/Api/Transaction/TransactionController.php b/app/Http/Controllers/Api/Transaction/TransactionController.php index 0fcffe2..184ed25 100644 --- a/app/Http/Controllers/Api/Transaction/TransactionController.php +++ b/app/Http/Controllers/Api/Transaction/TransactionController.php @@ -29,46 +29,52 @@ class TransactionController extends Controller $this->activityLogger = $activityLogger; } + // ponytail: VNPay auto-deposit disabled — nạp tiền hiện dùng luồng thủ công (ManualDepositController). public function handle(DepositRequest $request) { - $user = $request->user(); - $transRef = time(); - $uuid = Str::uuid(); - $publicCode = 'DEP-' . now()->format('Ymd') . '-' . strtoupper(Str::random(6)); - - $transaction = Transaction::create([ - 'uuid' => $uuid, - 'public_code' => $publicCode, - 'trans' => 'DEP' . $transRef, - 'amount' => $request->amount, - 'customer_id' => $user->id, - 'status' => TransactionStatus::PENDING->value, - ]); - - $paymentUrl = $this->vnpayService->createPaymentUrl([ - 'amount' => $request->amount, - 'transaction_ref' => $transRef, - 'transaction_uuid' => $uuid, - ]); - - $this->activityLogger->log( - event: 'deposit_vnpay', - logName: 'Transaction', - description: "Customer {$user->email} tạo yêu cầu nạp tiền VNPay: " . number_format($request->amount) . ' VNĐ - Mã: ' . $publicCode, - subject: $transaction, - newValues: ['public_code' => $publicCode, 'amount' => $request->amount, 'type' => 'vnpay'], - ); - return response()->json([ - 'error' => false, - 'message' => 'Tạo yêu cầu nạp tiền thành công', - 'data' => [ - 'transaction_id' => $transaction->trans, - 'transaction_uuid' => $uuid, - 'transaction_code' => $publicCode, - 'payment_url' => $paymentUrl, - ], - ]); + 'error' => true, + 'message' => 'Nạp tiền qua VNPay hiện không khả dụng. Vui lòng sử dụng nạp tiền thủ công.', + ], 410); + + // $user = $request->user(); + // $transRef = time(); + // $uuid = Str::uuid(); + // $publicCode = 'DEP-' . now()->format('Ymd') . '-' . strtoupper(Str::random(6)); + // + // $transaction = Transaction::create([ + // 'uuid' => $uuid, + // 'public_code' => $publicCode, + // 'trans' => 'DEP' . $transRef, + // 'amount' => $request->amount, + // 'customer_id' => $user->id, + // 'status' => TransactionStatus::PENDING->value, + // ]); + // + // $paymentUrl = $this->vnpayService->createPaymentUrl([ + // 'amount' => $request->amount, + // 'transaction_ref' => $transRef, + // 'transaction_uuid' => $uuid, + // ]); + // + // $this->activityLogger->log( + // event: 'deposit_vnpay', + // logName: 'Transaction', + // description: "Customer {$user->email} tạo yêu cầu nạp tiền VNPay: " . number_format($request->amount) . ' VNĐ - Mã: ' . $publicCode, + // subject: $transaction, + // newValues: ['public_code' => $publicCode, 'amount' => $request->amount, 'type' => 'vnpay'], + // ); + // + // return response()->json([ + // 'error' => false, + // 'message' => 'Tạo yêu cầu nạp tiền thành công', + // 'data' => [ + // 'transaction_id' => $transaction->trans, + // 'transaction_uuid' => $uuid, + // 'transaction_code' => $publicCode, + // 'payment_url' => $paymentUrl, + // ], + // ]); } protected function redirectToFrontend(string $status = 'failed', ?string $transactionCode = null): \Illuminate\Http\RedirectResponse @@ -85,110 +91,113 @@ class TransactionController extends Controller return redirect()->away($url . '?' . http_build_query($params)); } + // ponytail: VNPay auto-deposit disabled — nạp tiền hiện dùng luồng thủ công (ManualDepositController). public function returnCallback(Request $request) { - $data = $request->all(); - - $vnp_TxnRef = $data['vnp_TxnRef'] ?? null; - $vnp_Amount = $data['vnp_Amount'] ?? 0; - - FacadesLog::info('VNPay callback received', [ - 'vnp_TxnRef' => $vnp_TxnRef, - 'vnp_Amount' => $vnp_Amount, - 'user' => $request->user() ? $request->user()->id : 'not authenticated', - ]); - - $transaction = Transaction::where('trans', 'DEP' . $vnp_TxnRef)->first(); + return $this->redirectToFrontend('failed'); - if (!$transaction) { - return $this->redirectToFrontend('failed'); - } - - if ($transaction->status == TransactionStatus::COMPLETED->value) { - return $this->redirectToFrontend('success', $transaction->public_code); - } - - $verify = $this->vnpayService->verifyPayment($data); - - FacadesLog::info('VNPay callback debug', [ - 'isValid' => $verify['isValid'], - 'isSuccess' => $verify['isSuccess'] ?? false, - 'responseCode' => $data['vnp_ResponseCode'] ?? 'N/A', - ]); - - if (!$verify['isValid']) { - return $this->redirectToFrontend('failed', $transaction->public_code); - } - - if (!$verify['isSuccess']) { - $transaction->update(['status' => TransactionStatus::FAILED->value]); - return $this->redirectToFrontend('failed', $transaction->public_code); - } - - $originalAmount = $transaction->amount * 100; - if ($vnp_Amount != $originalAmount) { - $transaction->update(['status' => TransactionStatus::FAILED->value]); - return $this->redirectToFrontend('failed', $transaction->public_code); - } - - try { - DB::beginTransaction(); - - $transaction->update(['status' => TransactionStatus::COMPLETED->value]); - - $customer = Customer::find($transaction->customer_id); - - FacadesLog::info('Customer update debug', [ - 'customer_id' => $transaction->customer_id, - 'customer_found' => $customer ? true : false, - 'transaction_amount' => $transaction->amount, - 'current_total' => $customer->total ?? 'N/A', - 'current_credit' => $customer->credit ?? 'N/A', - ]); - - $customer->total += $transaction->amount; - $customer->credit += $transaction->amount / 100; - $customer->save(); - - FacadesLog::info('Customer after update', [ - 'new_total' => $customer->total, - 'new_credit' => $customer->credit, - ]); - - Log::create([ - 'agent' => 'api', - 'client_ip' => $request->ip(), - 'description' => 'Nạp tiền vào tài khoản: ' . number_format($transaction->amount) . ' VNĐ (+' . number_format($transaction->amount / 100) . ' credit)', - 'customer_id' => $transaction->customer_id, - ]); - - $this->activityLogger->log( - event: 'deposit_success', - logName: 'Transaction', - description: "Customer {$customer->email} nạp tiền VNPay thành công: " . number_format($transaction->amount) . ' VNĐ - Mã: ' . $transaction->public_code, - subject: $transaction, - newValues: ['amount' => $transaction->amount, 'new_total' => $customer->total, 'public_code' => $transaction->public_code], - ); - - DepositSuccessJob::dispatch([ - 'customer_email' => $customer->email, - 'customer_name' => $customer->fullname, - 'transaction_code' => $transaction->public_code, - 'amount' => $transaction->amount, - 'credit' => $transaction->amount / 100, - 'current_total' => $customer->total, - 'current_credit' => $customer->credit, - 'deposit_date' => now()->format('d/m/Y H:i:s'), - ]); - - DB::commit(); - } catch (\Exception $e) { - DB::rollBack(); - FacadesLog::error('Payment processing error: ' . $e->getMessage()); - return $this->redirectToFrontend('failed', $transaction->public_code); - } - - return $this->redirectToFrontend('success', $transaction->public_code); + // $data = $request->all(); + // + // $vnp_TxnRef = $data['vnp_TxnRef'] ?? null; + // $vnp_Amount = $data['vnp_Amount'] ?? 0; + // + // FacadesLog::info('VNPay callback received', [ + // 'vnp_TxnRef' => $vnp_TxnRef, + // 'vnp_Amount' => $vnp_Amount, + // 'user' => $request->user() ? $request->user()->id : 'not authenticated', + // ]); + // + // $transaction = Transaction::where('trans', 'DEP' . $vnp_TxnRef)->first(); + // + // if (!$transaction) { + // return $this->redirectToFrontend('failed'); + // } + // + // if ($transaction->status == TransactionStatus::COMPLETED->value) { + // return $this->redirectToFrontend('success', $transaction->public_code); + // } + // + // $verify = $this->vnpayService->verifyPayment($data); + // + // FacadesLog::info('VNPay callback debug', [ + // 'isValid' => $verify['isValid'], + // 'isSuccess' => $verify['isSuccess'] ?? false, + // 'responseCode' => $data['vnp_ResponseCode'] ?? 'N/A', + // ]); + // + // if (!$verify['isValid']) { + // return $this->redirectToFrontend('failed', $transaction->public_code); + // } + // + // if (!$verify['isSuccess']) { + // $transaction->update(['status' => TransactionStatus::FAILED->value]); + // return $this->redirectToFrontend('failed', $transaction->public_code); + // } + // + // $originalAmount = $transaction->amount * 100; + // if ($vnp_Amount != $originalAmount) { + // $transaction->update(['status' => TransactionStatus::FAILED->value]); + // return $this->redirectToFrontend('failed', $transaction->public_code); + // } + // + // try { + // DB::beginTransaction(); + // + // $transaction->update(['status' => TransactionStatus::COMPLETED->value]); + // + // $customer = Customer::find($transaction->customer_id); + // + // FacadesLog::info('Customer update debug', [ + // 'customer_id' => $transaction->customer_id, + // 'customer_found' => $customer ? true : false, + // 'transaction_amount' => $transaction->amount, + // 'current_total' => $customer->total ?? 'N/A', + // 'current_credit' => $customer->credit ?? 'N/A', + // ]); + // + // $customer->total += $transaction->amount; + // $customer->credit += $transaction->amount / 100; + // $customer->save(); + // + // FacadesLog::info('Customer after update', [ + // 'new_total' => $customer->total, + // 'new_credit' => $customer->credit, + // ]); + // + // Log::create([ + // 'agent' => 'api', + // 'client_ip' => $request->ip(), + // 'description' => 'Nạp tiền vào tài khoản: ' . number_format($transaction->amount) . ' VNĐ (+' . number_format($transaction->amount / 100) . ' credit)', + // 'customer_id' => $transaction->customer_id, + // ]); + // + // $this->activityLogger->log( + // event: 'deposit_success', + // logName: 'Transaction', + // description: "Customer {$customer->email} nạp tiền VNPay thành công: " . number_format($transaction->amount) . ' VNĐ - Mã: ' . $transaction->public_code, + // subject: $transaction, + // newValues: ['amount' => $transaction->amount, 'new_total' => $customer->total, 'public_code' => $transaction->public_code], + // ); + // + // DepositSuccessJob::dispatch([ + // 'customer_email' => $customer->email, + // 'customer_name' => $customer->fullname, + // 'transaction_code' => $transaction->public_code, + // 'amount' => $transaction->amount, + // 'credit' => $transaction->amount / 100, + // 'current_total' => $customer->total, + // 'current_credit' => $customer->credit, + // 'deposit_date' => now()->format('d/m/Y H:i:s'), + // ]); + // + // DB::commit(); + // } catch (\Exception $e) { + // DB::rollBack(); + // FacadesLog::error('Payment processing error: ' . $e->getMessage()); + // return $this->redirectToFrontend('failed', $transaction->public_code); + // } + // + // return $this->redirectToFrontend('success', $transaction->public_code); } public function getTransactions(Request $request) @@ -231,7 +240,7 @@ class TransactionController extends Controller public function verifyPayment(Request $request) { $transRef = $request->query('vnp_TxnRef'); - + if (!$transRef) { return response()->json([ 'error' => true, @@ -239,7 +248,9 @@ class TransactionController extends Controller ], 400); } - $transaction = Transaction::where('trans', 'DEP' . $transRef)->first(); + $transaction = Transaction::where('trans', 'DEP' . $transRef) + ->where('customer_id', $request->user()->id) + ->first(); if (!$transaction) { return response()->json([ @@ -325,7 +336,9 @@ class TransactionController extends Controller ], 400); } - $transaction = Transaction::where('public_code', $code)->first(); + $transaction = Transaction::where('public_code', $code) + ->where('customer_id', $request->user()->id) + ->first(); if (!$transaction) { return response()->json([ @@ -338,27 +351,13 @@ class TransactionController extends Controller 'error' => false, 'message' => 'Lấy thông tin giao dịch thành công', 'data' => [ - 'transaction' => [ - 'public_code' => $transaction->public_code, - 'amount' => $transaction->amount, - 'amount_formatted' => number_format($transaction->amount) . ' VNĐ', - 'status' => $transaction->status, - 'status_label' => TransactionStatus::from($transaction->status)->label(), - 'created_at' => $transaction->created_at->format('Y-m-d H:i:s'), - 'paid_at' => $transaction->updated_at->format('Y-m-d H:i:s'), - ], - 'customer' => [ - 'fullname' => $transaction->customer->fullname, - 'email' => $transaction->customer->email, - 'phone' => $transaction->customer->phone, - 'address' => $transaction->customer->address, - ], - 'balance' => [ - 'total' => $transaction->customer->total, - 'credit' => $transaction->customer->credit, - 'total_formatted' => number_format($transaction->customer->total) . ' VNĐ', - 'credit_formatted' => number_format($transaction->customer->credit) . ' điểm', - ], + 'public_code' => $transaction->public_code, + 'amount' => $transaction->amount, + 'amount_formatted' => number_format($transaction->amount) . ' VNĐ', + 'status' => $transaction->status, + 'status_label' => TransactionStatus::from($transaction->status)->label(), + 'created_at' => $transaction->created_at->format('Y-m-d H:i:s'), + 'paid_at' => $transaction->updated_at->format('Y-m-d H:i:s'), ], ]); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 60ef7b3..e942bd7 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -68,6 +68,7 @@ class Kernel extends HttpKernel 'Staff' => \App\Http\Middleware\StaffCheck::class, 'json.response' => \App\Http\Middleware\ForceJsonResponse::class, 'api.auth' => \App\Http\Middleware\ApiAuthCheck::class, + 'client.access-token' => \App\Http\Middleware\ClientAccessTokenCheck::class, 'perm' => \App\Http\Middleware\CheckPermission::class, 'compression' => \App\Http\Middleware\CompressionMiddleware::class, 'compression.advanced' => \App\Http\Middleware\AdvancedCompressionMiddleware::class, diff --git a/app/Http/Middleware/ClientAccessTokenCheck.php b/app/Http/Middleware/ClientAccessTokenCheck.php new file mode 100644 index 0000000..feb546b --- /dev/null +++ b/app/Http/Middleware/ClientAccessTokenCheck.php @@ -0,0 +1,50 @@ +header('x-access-token', ''); + $key = config('services.client_api.key'); + + if (!$key || !$this->isValid($token, $key)) { + return response()->json([ + 'error' => true, + 'message' => 'Invalid access token', + 'code' => 403, + ], 403); + } + + return $next($request); + } + + private function isValid(string $token, string $key): bool + { + $raw = base64_decode($token, true); + if ($raw === false || strlen($raw) <= 16) { + return false; + } + + $iv = substr($raw, 0, 16); + $ciphertext = substr($raw, 16); + + $plaintext = openssl_decrypt($ciphertext, 'aes-256-cbc', hex2bin($key), OPENSSL_RAW_DATA, $iv); + + if ($plaintext === false) { + return false; + } + + return hash_equals(config('services.client_api.secret'), $plaintext); + } +} diff --git a/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php b/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php index 2181673..4a1b49f 100644 --- a/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php +++ b/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php @@ -2,11 +2,31 @@ namespace App\Http\Resources\Api\Client\Service; +use App\Models\ModuleHostingPleskClient; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Facades\Crypt; class ServiceClientDetailResource extends JsonResource { + /** Plesk panel login (persisted, reusable) — separate from the one-time FTP password, which is never stored. */ + private function resolvePleskCredentials($hosting): ?array + { + $client = ModuleHostingPleskClient::query() + ->where('customer_id', $hosting->customer_id) + ->where('dns_manager_server_id', $hosting->dns_manager_server_id) + ->first(); + + if (!$client) { + return null; + } + + return [ + 'login' => $client->login, + 'password' => Crypt::decryptString($client->password), + ]; + } + /** * Transform the resource into an array. * @@ -50,6 +70,8 @@ class ServiceClientDetailResource extends JsonResource $order = $orderItem?->order; $vm = $this->resource->vmware; $product = $orderItem?->product; + $hosting = $this->resource->hostingAccount; + $pleskCredentials = $hosting ? $this->resolvePleskCredentials($hosting) : null; return [ 'id' => $this->id, @@ -81,6 +103,17 @@ class ServiceClientDetailResource extends JsonResource 'ip' => $vm->ip ?? null, 'dns' => $vm->dns ?? null, ] : null, + 'hosting' => $hosting ? [ + 'ip_address' => $hosting->server?->ip_address, + 'plesk_url' => $hosting->server?->ip_address + ? "http://{$hosting->server->ip_address}:{$hosting->server->panel_port}/" + : null, + 'hosting_plan' => $hosting->hosting_plan, + 'ftp_username' => $hosting->ftp_username, + 'username' => $pleskCredentials['login'] ?? null, + 'password' => $pleskCredentials['password'] ?? null, + 'status' => $hosting->status, + ] : null, ]; } } diff --git a/app/Http/Resources/Api/Service/VpsServiceResource.php b/app/Http/Resources/Api/Service/VpsServiceResource.php index 8fde36f..635d15d 100644 --- a/app/Http/Resources/Api/Service/VpsServiceResource.php +++ b/app/Http/Resources/Api/Service/VpsServiceResource.php @@ -58,7 +58,7 @@ class VpsServiceResource extends JsonResource 'name' => $product->name, 'slug' => $product->slug, 'product_type' => $product->product_type, - 'page_type' => 'service_vps', + 'page_type' => 'service_' . str_replace(' ', '_', $product->product_type), ] : null, 'specs' => $this->resolveSpecs($product), ]; diff --git a/app/Http/Services/vCenter/PowerShellService.php b/app/Http/Services/vCenter/PowerShellService.php index 5b5d405..3a0742c 100644 --- a/app/Http/Services/vCenter/PowerShellService.php +++ b/app/Http/Services/vCenter/PowerShellService.php @@ -256,11 +256,15 @@ PS; $data = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE) { + \Illuminate\Support\Facades\Log::error('changeRDPPortWindows: failed to parse PowerCLI output', [ + 'vm_name' => $vmName, + 'raw_output' => $raw, + 'json_error' => json_last_error_msg(), + ]); + return [ 'success' => false, 'error' => 'JSON parsing failed', - 'raw_output' => $raw, - 'json_error' => json_last_error_msg() ]; } diff --git a/app/Jobs/Hosting/ProvisionHostingAccountJob.php b/app/Jobs/Hosting/ProvisionHostingAccountJob.php index 9686441..58d2df1 100644 --- a/app/Jobs/Hosting/ProvisionHostingAccountJob.php +++ b/app/Jobs/Hosting/ProvisionHostingAccountJob.php @@ -4,9 +4,8 @@ declare(strict_types=1); namespace App\Jobs\Hosting; -use App\Enums\Server\ServerType; -use App\Models\DnsServer; use App\Models\OrderItem; +use App\Models\ServicePlan; use App\Services\Plesk\PleskHostingService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -49,14 +48,50 @@ class ProvisionHostingAccountJob implements ShouldQueue return; } - $server = DnsServer::query() - ->where('status', 'activated') - ->whereIn('type', [ServerType::HOSTING->value, ServerType::BOTH->value]) - ->first(); + $servicePlanId = $this->item->product?->service_plan_id; + $servicePlan = $servicePlanId + ? ServicePlan::with(['plesk', 'dnsServer'])->find($servicePlanId) + : null; + + $hostingPlan = $servicePlan?->plesk?->plesk_plan_name; + $server = $servicePlan?->dnsServer; + + Log::channel('Custom')->info('ProvisionHostingAccountJob resolved service plan', [ + 'order_item_id' => $this->item->id, + 'product_id' => $this->item->product_id, + 'product_name' => $this->item->product?->name, + 'service_plan_id' => $servicePlanId, + 'plesk_plan_name' => $hostingPlan, + 'dns_manager_server_id' => $server?->id, + 'dns_manager_server_ip' => $server?->ip_address, + ]); + + if (! $hostingPlan) { + Log::channel('Custom')->error('ProvisionHostingAccountJob failed: no service_plan_plesk mapping for this product', [ + 'order_item_id' => $this->item->id, + 'product_id' => $this->item->product_id, + 'service_plan_id' => $servicePlanId, + ]); + + return; + } if (! $server) { - Log::channel('Custom')->error('ProvisionHostingAccountJob failed: no active hosting server available', [ - 'order_item_id' => $this->item->id, + Log::channel('Custom')->error('ProvisionHostingAccountJob failed: service plan has no dns_manager_server_id configured', [ + 'order_item_id' => $this->item->id, + 'product_id' => $this->item->product_id, + 'service_plan_id' => $servicePlanId, + ]); + + return; + } + + if (! $server->servesHosting()) { + Log::channel('Custom')->error('ProvisionHostingAccountJob failed: configured server does not serve hosting', [ + 'order_item_id' => $this->item->id, + 'service_plan_id' => $servicePlanId, + 'dns_manager_server_id' => $server->id, + 'server_type' => $server->type, ]); return; @@ -67,12 +102,13 @@ class ProvisionHostingAccountJob implements ShouldQueue 'service_id' => $service->id, 'dns_manager_server_id' => $server->id, 'domain_name' => $domainName, - 'hosting_plan' => $this->item->product?->name ?? 'Default', + 'hosting_plan' => $hostingPlan, ]); if ($result['error']) { Log::channel('Custom')->error('ProvisionHostingAccountJob failed', [ 'order_item_id' => $this->item->id, + 'hosting_plan' => $hostingPlan, 'message' => $result['message'] ?? 'Unknown error', ]); diff --git a/app/Models/DnsServer.php b/app/Models/DnsServer.php index 38c3358..ef25567 100644 --- a/app/Models/DnsServer.php +++ b/app/Models/DnsServer.php @@ -16,6 +16,7 @@ class DnsServer extends Model protected $fillable = [ 'name', 'ip_address', + 'panel_port', 'password', 'username', 'api_key', diff --git a/app/Models/Service.php b/app/Models/Service.php index b79b5bf..b6cf1e9 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -70,6 +70,12 @@ class Service extends Model return $this->belongsTo(ModuleVmware::class, 'vm_id'); } + /** The Plesk hosting account associated with this service, if type = hosting. */ + public function hostingAccount() + { + return $this->hasOne(ModuleHostingAccount::class, 'service_id'); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- diff --git a/app/Models/ServicePlan.php b/app/Models/ServicePlan.php index fc5ef6c..f78c578 100644 --- a/app/Models/ServicePlan.php +++ b/app/Models/ServicePlan.php @@ -12,10 +12,16 @@ class ServicePlan extends Model 'ram', 'disk', 'domain', + 'dns_manager_server_id', ]; public function plesk() { return $this->hasOne(ServicePlanPlesk::class); } + + public function dnsServer() + { + return $this->belongsTo(DnsServer::class, 'dns_manager_server_id'); + } } diff --git a/app/Repositories/Api/Order/OrderRepository.php b/app/Repositories/Api/Order/OrderRepository.php index 98e6903..8cb5632 100644 --- a/app/Repositories/Api/Order/OrderRepository.php +++ b/app/Repositories/Api/Order/OrderRepository.php @@ -70,6 +70,15 @@ class OrderRepository extends BaseApiRepository implements OrderRepositoryInterf return $this->model->where('order_num', $orderNum)->where('customer_id', $customerId)->first(); } + // ------------------------------------------------------------------------- + // findForCustomer + // ------------------------------------------------------------------------- + + public function findForCustomer(int $id, int $customerId): ?Order + { + return $this->model->where('id', $id)->where('customer_id', $customerId)->first(); + } + // ------------------------------------------------------------------------- // getOrders // ------------------------------------------------------------------------- diff --git a/app/Repositories/Api/ServiceClient/ServiceClientRepository.php b/app/Repositories/Api/ServiceClient/ServiceClientRepository.php index 58f0e95..9953a5e 100644 --- a/app/Repositories/Api/ServiceClient/ServiceClientRepository.php +++ b/app/Repositories/Api/ServiceClient/ServiceClientRepository.php @@ -29,9 +29,7 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient ->orderBy('started_at', 'desc'); if ($productType) { - $query->whereHas('product', function ($builder) use ($productType) { - $builder->where('product_type', $productType); - }); + $query->where('type', $productType); } return $query->paginate($limit); @@ -66,6 +64,7 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient 'orderItem.product', 'orderItem.order', 'vmware.ipAddress', + 'hostingAccount.server', ]) ->where('uuid', $uuid) ->where('customer_id', $customerId) diff --git a/app/Services/Api/Domain/DomainOrderService.php b/app/Services/Api/Domain/DomainOrderService.php index 383ff2a..1a6ac8f 100644 --- a/app/Services/Api/Domain/DomainOrderService.php +++ b/app/Services/Api/Domain/DomainOrderService.php @@ -138,6 +138,7 @@ class DomainOrderService $service = Service::create([ 'customer_id' => $lockedCustomer->id, 'product_id' => $product->id, + 'type' => ProductTypeEnums::DOMAIN->value, 'order_item_id' => $orderItem->id, 'invoice_id' => $invoice->id, 'server_name' => $domainName, diff --git a/app/Services/Api/Order/OrderService.php b/app/Services/Api/Order/OrderService.php index 421b51f..0a243c5 100644 --- a/app/Services/Api/Order/OrderService.php +++ b/app/Services/Api/Order/OrderService.php @@ -157,6 +157,7 @@ class OrderService $service = Service::create([ 'customer_id' => $customer->id, 'product_id' => $item->product_id, + 'type' => $item->product?->product_type, 'order_item_id' => $item->id, 'invoice_id' => $invoice?->id, 'server_name' => $item->server_name, diff --git a/app/Services/Plesk/PleskHostingService.php b/app/Services/Plesk/PleskHostingService.php index 7f52e06..10c520e 100644 --- a/app/Services/Plesk/PleskHostingService.php +++ b/app/Services/Plesk/PleskHostingService.php @@ -10,6 +10,7 @@ use App\Models\DnsServer; use App\Models\ModuleHostingAccount; use App\Repositories\DnsManager\Domain\DnsDomainRepositoryInterface; use App\Repositories\Module\HostingAccount\HostingAccountRepositoryInterface; +use Illuminate\Support\Str; /** * Phase 1 orchestration: Create Customer -> Create Hosting Subscription -> @@ -54,7 +55,8 @@ class PleskHostingService $pleskLogin = $client['data']['login']; $pleskPassword = $client['data']['password']; - $ftpLogin = str_replace('.', '_', $data['domain_name']); + // Suffix avoids a collision if the same domain is re-provisioned (e.g. after cancellation). + $ftpLogin = str_replace('.', '_', $data['domain_name']) . '_' . Str::lower(Str::random(4)); // Not persisted anywhere — the only record of it is this response, once. $ftpPassword = $data['ftp_password'] ?? Helper::general_password(); diff --git a/config/services.php b/config/services.php index fb504fe..c0447b4 100644 --- a/config/services.php +++ b/config/services.php @@ -39,4 +39,10 @@ return [ 'wordpress_posts_url' => env('DOCS_WORDPRESS_POSTS_URL', 'https://tailieu.tgs.com.vn/wp-json/wp/v2/posts'), ], + 'client_api' => [ + // openssl rand -hex 32 + 'key' => env('CLIENT_API_ACCESS_KEY'), + 'secret' => env('CLIENT_API_ACCESS_SECRET', 'tgs-client-api'), + ], + ]; diff --git a/database/migrations/2026_08_17_104858_add_type_to_services_table.php b/database/migrations/2026_08_17_104858_add_type_to_services_table.php new file mode 100644 index 0000000..708901e --- /dev/null +++ b/database/migrations/2026_08_17_104858_add_type_to_services_table.php @@ -0,0 +1,31 @@ +string('type', 50)->nullable()->after('product_id')->index(); + }); + + // Backfill existing rows from products.product_type (vps / hosting / domain / ...). + DB::statement(<<<'SQL' + UPDATE services + JOIN products ON products.id = services.product_id + SET services.type = products.product_type + WHERE services.type IS NULL + SQL); + } + + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } +}; diff --git a/database/migrations/2026_08_17_105733_add_dns_manager_server_id_to_service_plans_table.php b/database/migrations/2026_08_17_105733_add_dns_manager_server_id_to_service_plans_table.php new file mode 100644 index 0000000..4891031 --- /dev/null +++ b/database/migrations/2026_08_17_105733_add_dns_manager_server_id_to_service_plans_table.php @@ -0,0 +1,23 @@ +foreignId('dns_manager_server_id')->nullable()->after('domain') + ->constrained('dns_manager_servers')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('service_plans', function (Blueprint $table) { + $table->dropConstrainedForeignId('dns_manager_server_id'); + }); + } +}; diff --git a/database/migrations/2026_08_17_111510_add_panel_port_to_dns_manager_servers_table.php b/database/migrations/2026_08_17_111510_add_panel_port_to_dns_manager_servers_table.php new file mode 100644 index 0000000..0c50780 --- /dev/null +++ b/database/migrations/2026_08_17_111510_add_panel_port_to_dns_manager_servers_table.php @@ -0,0 +1,22 @@ +unsignedInteger('panel_port')->default(8880)->after('ip_address'); + }); + } + + public function down(): void + { + Schema::table('dns_manager_servers', function (Blueprint $table) { + $table->dropColumn('panel_port'); + }); + } +}; diff --git a/routes/client.php b/routes/client.php index 190c077..432e53e 100644 --- a/routes/client.php +++ b/routes/client.php @@ -24,6 +24,7 @@ use Illuminate\Support\Facades\Route; Route::prefix('v1') ->as('api.') + ->middleware(['client.access-token']) ->group(function () { // Auth Route::prefix('client') @@ -263,7 +264,7 @@ Route::prefix('v1') Route::get('v1/payment/vnpay/return', [TransactionController::class, 'returnCallback']); Route::prefix('v1/system') - ->middleware(['api.auth']) + ->middleware(['client.access-token', 'api.auth']) ->group(function () { // VM power & network operations Route::controller(VmController::class)->group(function () { diff --git a/yarn.lock b/yarn.lock index b8ae6ac..7e0cc91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4597,11 +4597,16 @@ jquery.flot@^0.8.3: resolved "https://registry.npmjs.org/jquery.flot/-/jquery.flot-0.8.3.tgz" integrity sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg== -jquery@*, jquery@>=1.2.6, jquery@>=1.7, jquery@>=1.8.3, jquery@^4.0.0: +jquery@*, jquery@>=1.2.6, jquery@>=1.7, jquery@>=1.8.3: version "4.0.0" resolved "https://registry.npmjs.org/jquery/-/jquery-4.0.0.tgz" integrity sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg== +jquery@3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" + integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== + jquery@^3.7.1: version "3.7.1" resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz"