update rm deadcode
This commit is contained in:
@@ -9,11 +9,15 @@ use App\Services\ActivityLogger;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||
use Tymon\JWTAuth\Exceptions\JWTException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
private const LOGIN_MAX_ATTEMPTS = 5;
|
||||
private const LOGIN_DECAY_SECONDS = 60;
|
||||
|
||||
public function __construct(private readonly ActivityLogger $activityLogger) {}
|
||||
|
||||
public function login(Request $request): JsonResponse
|
||||
@@ -23,9 +27,20 @@ class AuthController extends Controller
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
$throttleKey = 'admin-login:' . strtolower($request->string('username')->toString()) . '|' . $request->ip();
|
||||
|
||||
if (RateLimiter::tooManyAttempts($throttleKey, self::LOGIN_MAX_ATTEMPTS)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Bạn đã đăng nhập sai quá nhiều lần. Vui lòng thử lại sau ít phút.',
|
||||
], 429);
|
||||
}
|
||||
|
||||
$token = Auth::guard('admin')->attempt($request->only('username', 'password'));
|
||||
|
||||
if (!$token) {
|
||||
RateLimiter::hit($throttleKey, self::LOGIN_DECAY_SECONDS);
|
||||
|
||||
$this->activityLogger->log(
|
||||
event: 'login_failed',
|
||||
logName: 'auth',
|
||||
@@ -37,6 +52,8 @@ class AuthController extends Controller
|
||||
], 401);
|
||||
}
|
||||
|
||||
RateLimiter::clear($throttleKey);
|
||||
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
$this->activityLogger->logAuth('login', 'admin', $user->id, $user->username ?? $user->email);
|
||||
|
||||
@@ -362,6 +362,8 @@ class CustomerController extends Controller
|
||||
'started_at' => 'nullable|date',
|
||||
'expires_at' => 'nullable|date',
|
||||
'note' => 'nullable|string',
|
||||
'requirements' => 'nullable|string',
|
||||
'feedback' => 'nullable|string',
|
||||
'price' => 'nullable|numeric|min:0',
|
||||
'os_id' => 'nullable|integer|exists:os,id',
|
||||
'vm_cpu' => 'nullable|integer|min:1',
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\DnsManager;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Registrar;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RegistrarController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => Registrar::where('status', 'active')->orderBy('name')->get(['id', 'name', 'short_name', 'website']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,11 @@ class EmployeeController extends Controller
|
||||
|
||||
$request->validated();
|
||||
|
||||
$guardError = $this->guardEmployeeMutation($employee, $request->role);
|
||||
if ($guardError) {
|
||||
return $guardError;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
@@ -138,6 +143,14 @@ class EmployeeController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
$currentUser = auth('admin')->user();
|
||||
if ($employee->hasRole('Full Administrator') && !$currentUser->hasRole('Full Administrator')) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Bạn không có quyền xoá tài khoản Full Administrator.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$this->employeeRepository->delete($id);
|
||||
@@ -156,6 +169,34 @@ class EmployeeController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
private function guardEmployeeMutation($employee, string $requestedRole): ?JsonResponse
|
||||
{
|
||||
$currentUser = auth('admin')->user();
|
||||
|
||||
if ($employee->hasRole('Full Administrator') && !$currentUser->hasRole('Full Administrator')) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Bạn không có quyền sửa tài khoản Full Administrator.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
if ($employee->id === $currentUser->id && !$currentUser->hasRole($requestedRole)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Bạn không thể tự đổi vai trò của chính mình.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
if (in_array($requestedRole, ['Administrator', 'Full Administrator'], true) && !$currentUser->hasRole('Full Administrator')) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chỉ Full Administrator mới có quyền gán vai trò này.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function roles(): JsonResponse
|
||||
{
|
||||
$roles = Role::where('guard_name', 'admin')->get(['id', 'name']);
|
||||
|
||||
@@ -194,13 +194,13 @@ class ProductController extends Controller
|
||||
{
|
||||
$currency = $data['currency'] ?? 'VND';
|
||||
$cycles = [
|
||||
'monthly' => 'price_monthly',
|
||||
'quarterly' => 'price_quarterly',
|
||||
'semiannually' => 'price_semiannually',
|
||||
'annually' => 'price_annually',
|
||||
'biennially' => 'price_biennially',
|
||||
'triennially' => 'price_triennially',
|
||||
'onetime' => 'price_onetime',
|
||||
'monthly' => 'price_monthly',
|
||||
'quarterly' => 'price_quarterly',
|
||||
'semi_annually' => 'price_semiannually',
|
||||
'annually' => 'price_annually',
|
||||
'biennially' => 'price_biennially',
|
||||
'triennially' => 'price_triennially',
|
||||
'onetime' => 'price_onetime',
|
||||
];
|
||||
|
||||
foreach ($cycles as $cycle => $field) {
|
||||
|
||||
@@ -36,7 +36,7 @@ class ServiceController extends Controller
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$service = Service::with(['customer', 'product', 'orderItem', 'invoice', 'vmware.os.osGroup', 'addons'])
|
||||
$service = Service::with(['customer', 'product', 'orderItem', 'invoice', 'vmware.os.osGroup', 'addons', ...Service::HOSTING_ACCOUNT_RELATIONS])
|
||||
->findOrFail($id);
|
||||
|
||||
ModuleVmware::resolveVirtualMachineIds([$service->vmware]);
|
||||
|
||||
@@ -186,7 +186,7 @@ class SettingController extends Controller
|
||||
public function uploadMedia(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:jpg,jpeg,png,webp,svg|max:5120',
|
||||
'file' => 'required|file|mimes:jpg,jpeg,png,webp|max:5120',
|
||||
]);
|
||||
|
||||
$path = $request->file('file')->store('system-config', 'public');
|
||||
|
||||
@@ -64,7 +64,7 @@ class AdminTransactionController extends Controller
|
||||
'type' => $t->type,
|
||||
'status' => $t->status,
|
||||
'status_label' => TransactionStatus::from((int) $t->status)->label(),
|
||||
'proof_image' => $t->proof_image ? Storage::disk('public')->url($t->proof_image) : null,
|
||||
'proof_image' => $t->proof_image_urls[0] ?? null,
|
||||
'created_at' => $t->created_at?->toISOString(),
|
||||
'customer' => $t->customer ? [
|
||||
'uuid' => $t->customer->uuid,
|
||||
@@ -287,4 +287,21 @@ class AdminTransactionController extends Controller
|
||||
'message' => 'Đã từ chối giao dịch',
|
||||
]);
|
||||
}
|
||||
|
||||
public function proofImage(Transaction $transaction, int $index = 0)
|
||||
{
|
||||
abort_unless($transaction->proof_image, 404);
|
||||
|
||||
$paths = json_decode((string) $transaction->proof_image, true);
|
||||
$paths = is_array($paths) ? array_values($paths) : [$transaction->proof_image];
|
||||
$path = $paths[$index] ?? null;
|
||||
|
||||
abort_unless(is_string($path) && $path !== '', 404);
|
||||
|
||||
$disk = Storage::disk('local')->exists($path) ? 'local' : 'public';
|
||||
|
||||
abort_unless(Storage::disk($disk)->exists($path), 404);
|
||||
|
||||
return Storage::disk($disk)->response($path, null, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Services\ActivityLogger;
|
||||
use App\Services\Api\Client\ClientAuthService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
/**
|
||||
* @OA\Tag(
|
||||
@@ -411,7 +412,7 @@ class ClientController extends Controller
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'token' => ['required', 'string', 'max:255'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
'password' => ['required', Password::min(10), 'confirmed'],
|
||||
]);
|
||||
|
||||
$customer = $this->authService->resetPassword($data);
|
||||
@@ -439,7 +440,17 @@ class ClientController extends Controller
|
||||
|
||||
abort_unless($path && str_starts_with($path, 'cccd/') && Storage::disk('local')->exists($path), 404);
|
||||
|
||||
return Storage::disk('local')->response($path, null, ['Cache-Control' => 'private, no-store']);
|
||||
$mime = match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
default => 'image/jpeg',
|
||||
};
|
||||
|
||||
return Storage::disk('local')->response($path, "cccd-{$side}", [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => "inline; filename=\"cccd-{$side}\"",
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(ClientUpdateRequest $request)
|
||||
|
||||
@@ -112,7 +112,9 @@ class DomainController extends Controller
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
\Log::error('DomainController::storeRecord error: ' . $e->getMessage());
|
||||
|
||||
return response()->json(['error' => true, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500);
|
||||
}
|
||||
|
||||
$customer = $request->user();
|
||||
|
||||
@@ -9,7 +9,7 @@ use Illuminate\Http\Request;
|
||||
|
||||
trait ResolvesVm
|
||||
{
|
||||
protected function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse
|
||||
protected function resolveVmById(int|string $productId, Request $request, bool $requireActiveService = true): ModuleVmware|JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$vm = null;
|
||||
@@ -42,6 +42,13 @@ trait ResolvesVm
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
if ($requireActiveService) {
|
||||
$service = $vm->service ?? Service::where('vm_id', $vm->vmware_id)->first();
|
||||
if ($service && $service->status !== 'active') {
|
||||
return response()->json(['error' => true, 'message' => 'Dịch vụ không ở trạng thái hoạt động, không thể thao tác.'], 403);
|
||||
}
|
||||
}
|
||||
|
||||
return $vm;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,8 +105,16 @@ class OrderControllerRefactor extends Controller
|
||||
],
|
||||
201,
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json(
|
||||
['success' => false, 'message' => collect($e->errors())->flatten()->first() ?? $e->getMessage()],
|
||||
422,
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 400);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('OrderControllerRefactor::storeOrder error: ' . $e->getMessage());
|
||||
return response()->json(['success' => false, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +128,11 @@ class OrderControllerRefactor extends Controller
|
||||
|
||||
try {
|
||||
$this->orderService->pay($order, $request->payment_method);
|
||||
} catch (\Throwable $e) {
|
||||
// Số dư không đủ / đơn đã thanh toán là lỗi nghiệp vụ, không phải lỗi server —
|
||||
// trả 422 cho khớp với payClientInvoiceByNumber.
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 422);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('OrderControllerRefactor::payOrder error: ' . $e->getMessage());
|
||||
return response()->json(['success' => false, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500);
|
||||
}
|
||||
|
||||
$this->activityLogger->log(
|
||||
@@ -250,8 +259,11 @@ class OrderControllerRefactor extends Controller
|
||||
: 'Thanh toán hóa đơn thành công.',
|
||||
'data' => new InvoiceDetailResource($paidInvoice),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 422);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('OrderControllerRefactor::payClientInvoiceByNumber error: ' . $e->getMessage());
|
||||
return response()->json(['success' => false, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,33 +286,33 @@ class ProductCategoryController extends Controller
|
||||
private function formatProduct($product): array
|
||||
{
|
||||
$billingCycleLabels = [
|
||||
'monthly' => 'Hàng tháng',
|
||||
'quarterly' => 'Hàng quý',
|
||||
'semiannually' => 'Nửa năm',
|
||||
'annually' => 'Hàng năm',
|
||||
'biennially' => '2 năm',
|
||||
'triennially' => '3 năm',
|
||||
'onetime' => 'Một lần',
|
||||
'monthly' => 'Hàng tháng',
|
||||
'quarterly' => 'Hàng quý',
|
||||
'semi_annually' => 'Nửa năm',
|
||||
'annually' => 'Hàng năm',
|
||||
'biennially' => '2 năm',
|
||||
'triennially' => '3 năm',
|
||||
'onetime' => 'Một lần',
|
||||
];
|
||||
|
||||
$periodMonths = [
|
||||
'monthly' => 1,
|
||||
'quarterly' => 3,
|
||||
'semiannually' => 6,
|
||||
'annually' => 12,
|
||||
'biennially' => 24,
|
||||
'triennially' => 36,
|
||||
'onetime' => 0,
|
||||
'monthly' => 1,
|
||||
'quarterly' => 3,
|
||||
'semi_annually' => 6,
|
||||
'annually' => 12,
|
||||
'biennially' => 24,
|
||||
'triennially' => 36,
|
||||
'onetime' => 0,
|
||||
];
|
||||
|
||||
$periodUnits = [
|
||||
'monthly' => 'month',
|
||||
'quarterly' => 'quarter',
|
||||
'semiannually' => 'semi_year',
|
||||
'annually' => 'year',
|
||||
'biennially' => 'biennially',
|
||||
'triennially' => 'triennially',
|
||||
'onetime' => 'none',
|
||||
'monthly' => 'month',
|
||||
'quarterly' => 'quarter',
|
||||
'semi_annually' => 'semi_year',
|
||||
'annually' => 'year',
|
||||
'biennially' => 'biennially',
|
||||
'triennially' => 'triennially',
|
||||
'onetime' => 'none',
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
@@ -191,7 +191,9 @@ class ServiceController extends Controller
|
||||
|
||||
public function getDetailServiceCustomer(Request $request, $uuid)
|
||||
{
|
||||
$service = Service::where('uuid', $uuid)->first();
|
||||
$service = Service::where('uuid', $uuid)
|
||||
->where('customer_id', $request->user()->id)
|
||||
->first();
|
||||
|
||||
if (!$service) {
|
||||
return response()->json([
|
||||
|
||||
@@ -76,7 +76,7 @@ class SnapshotController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVmById($productParam, $request);
|
||||
$vm = $this->resolveVmById($productParam, $request, requireActiveService: false);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\ChangeIpVMJob;
|
||||
use App\Jobs\VirtualMachine\ChangeVmPasswordJob;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Os;
|
||||
use App\Models\Service;
|
||||
use App\Services\ActivityLogger;
|
||||
use App\Services\VirtualMachine\VmIpReservationService;
|
||||
use App\Services\VmProvisionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -21,9 +23,7 @@ use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class VmController extends Controller
|
||||
{
|
||||
use ResolvesVm {
|
||||
ResolvesVm::resolveVmById as resolveVmForClient;
|
||||
}
|
||||
use ResolvesVm;
|
||||
|
||||
public function __construct(protected ActivityLogger $activityLogger) {}
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -114,43 +114,67 @@ class VmController extends Controller
|
||||
// Change IP
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* GET /v1/system/vm-ip-options
|
||||
*
|
||||
* Query: server_name string Service.server_name
|
||||
* Lists free IP pool entries in the same network as the VM's current IP.
|
||||
*/
|
||||
public function ipOptions(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'server_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$service = $this->findOwnedServiceByServerName($validated['server_name'], $request);
|
||||
if ($service instanceof JsonResponse) {
|
||||
return $service;
|
||||
}
|
||||
|
||||
$vm = $service->orderItem?->order?->vmWare?->first();
|
||||
if (!$vm) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404);
|
||||
}
|
||||
|
||||
$networkId = $vm->ipAddress?->module_network_id;
|
||||
if (!$networkId) {
|
||||
return response()->json(['success' => false, 'message' => 'Không xác định được mạng của VM.'], 422);
|
||||
}
|
||||
|
||||
$ips = app(VmIpReservationService::class)->freeIpsInNetwork($networkId);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $ips->map(fn (IpPool $ip) => [
|
||||
'ip_address' => $ip->ip_address,
|
||||
'gateway' => $ip->gateway,
|
||||
])->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/system/vm-change-ip
|
||||
*
|
||||
* Body:
|
||||
* server_name string Service.server_name (e.g. "xasdf.com.vnx")
|
||||
* ip_address string New IP to assign
|
||||
* gateway string (optional) Gateway — falls back to existing IP record
|
||||
* ip_address string New IP to assign — must be a free IP from the VM's network pool
|
||||
*/
|
||||
/**
|
||||
* Dispatches ChangeIpVMJob — returns immediately, runs async.
|
||||
*/
|
||||
public function changeIp(Request $request): JsonResponse
|
||||
public function changeIp(Request $request, VmIpReservationService $ipReservation): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'server_name' => ['required', 'string', 'max:255'],
|
||||
'ip_address' => ['required', 'ip'],
|
||||
'gateway' => ['nullable', 'ip'],
|
||||
'dns' => ['nullable', 'ip'],
|
||||
'guest_user' => ['required', 'string', 'max:255'],
|
||||
'guest_password' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$customer = $request->user();
|
||||
|
||||
// Look up Service by server_name + owner
|
||||
// VM chain: Service → orderItem → order → vmWare (same as ServiceClientDetailResource)
|
||||
$service = Service::with([
|
||||
'orderItem.order.vmWare.vCenter',
|
||||
'orderItem.order.vmWare.ipAddress.host.datacenter.vCenter',
|
||||
'orderItem.os.osGroup',
|
||||
])
|
||||
->where('server_name', $validated['server_name'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$service) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404);
|
||||
$service = $this->findOwnedServiceByServerName($validated['server_name'], $request);
|
||||
if ($service instanceof JsonResponse) {
|
||||
return $service;
|
||||
}
|
||||
|
||||
$vm = $service->orderItem?->order?->vmWare?->first();
|
||||
@@ -159,29 +183,47 @@ class VmController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404);
|
||||
}
|
||||
|
||||
$networkId = $vm->ipAddress?->module_network_id;
|
||||
if (!$networkId) {
|
||||
return response()->json(['success' => false, 'message' => 'Không xác định được mạng của VM.'], 422);
|
||||
}
|
||||
|
||||
$reservation = $ipReservation->reserveInNetwork($networkId, $validated['ip_address'], $service->id);
|
||||
if (!$reservation) {
|
||||
return response()->json(['success' => false, 'message' => 'IP không thuộc pool khả dụng hoặc đã được sử dụng.'], 422);
|
||||
}
|
||||
|
||||
$oldIpPoolId = IpPool::where('module_network_id', $networkId)
|
||||
->where('service_id', $service->id)
|
||||
->where('status', 'assigned')
|
||||
->where('id', '!=', $reservation['ip_pool_id'])
|
||||
->value('id');
|
||||
|
||||
// Auto-detect OS type from the service's order item OS group
|
||||
$osGroup = strtolower($service->orderItem?->os?->osGroup?->name ?? '');
|
||||
$osType = (str_contains($osGroup, 'linux') || str_contains($osGroup, 'ubuntu') || str_contains($osGroup, 'centos'))
|
||||
? 'linux'
|
||||
: 'windows';
|
||||
|
||||
$gateway = $validated['gateway'] ?? '';
|
||||
$dns = $validated['dns'] ?? '8.8.8.8';
|
||||
$dns = $validated['dns'] ?? '8.8.8.8';
|
||||
|
||||
// ponytail: don't release — lock expires after 5 min while the async job runs
|
||||
$lock = $this->acquireVmLock($vm, 300);
|
||||
if (!$lock) {
|
||||
$ipReservation->releaseByPoolId($reservation['ip_pool_id']);
|
||||
return $this->vmLocked();
|
||||
}
|
||||
|
||||
ChangeIpVMJob::dispatch(
|
||||
$vm->id,
|
||||
$validated['ip_address'],
|
||||
$gateway,
|
||||
$reservation['gateway'],
|
||||
$osType,
|
||||
$dns,
|
||||
$validated['guest_user'],
|
||||
$validated['guest_password'],
|
||||
$reservation['ip_pool_id'],
|
||||
$oldIpPoolId,
|
||||
);
|
||||
|
||||
$this->activityLogger->log(
|
||||
@@ -189,7 +231,7 @@ class VmController extends Controller
|
||||
logName: 'VM',
|
||||
description: "Customer {$request->user()->email} yêu cầu đổi IP VM: {$vm->name} → {$validated['ip_address']}",
|
||||
subject: $vm,
|
||||
newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $gateway, 'os_type' => $osType],
|
||||
newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $reservation['gateway'], 'os_type' => $osType],
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
@@ -198,6 +240,35 @@ class VmController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a Service by server_name, verify ownership and active status.
|
||||
* Returns JsonResponse on error, Service on success.
|
||||
*/
|
||||
private function findOwnedServiceByServerName(string $serverName, Request $request): Service|JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
|
||||
// VM chain: Service → orderItem → order → vmWare (same as ServiceClientDetailResource)
|
||||
$service = Service::with([
|
||||
'orderItem.order.vmWare.vCenter',
|
||||
'orderItem.order.vmWare.ipAddress.host.datacenter.vCenter',
|
||||
'orderItem.os.osGroup',
|
||||
])
|
||||
->where('server_name', $serverName)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$service) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404);
|
||||
}
|
||||
|
||||
if ($service->status !== 'active') {
|
||||
return response()->json(['success' => false, 'message' => 'Dịch vụ không ở trạng thái hoạt động, không thể thao tác.'], 403);
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Change Port (RDP)
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -319,7 +390,7 @@ class VmController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVmForClient($productId, $request);
|
||||
$vm = $this->resolveVmById($productId, $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
@@ -431,7 +502,7 @@ class VmController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVmById($productId, $request);
|
||||
$vm = $this->resolveVmById($productId, $request, requireActiveService: false);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
@@ -462,60 +533,6 @@ class VmController extends Controller
|
||||
// Shared helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load a ModuleVmware by ID (int), vmware_id (string), or Service UUID.
|
||||
* Verify the authenticated user owns it.
|
||||
* Returns JsonResponse on error, ModuleVmware on success.
|
||||
*/
|
||||
private function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$vm = null;
|
||||
|
||||
// Check if it's a UUID (Service UUID)
|
||||
if (is_string($productId) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productId)) {
|
||||
$service = Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
|
||||
->where('uuid', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
if ($service && $service->vmware) {
|
||||
$vm = $service->vmware;
|
||||
}
|
||||
} elseif (is_numeric($productId)) {
|
||||
// Numeric ID
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->where('id', (int) $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
} else {
|
||||
// vmware_id string (e.g., "vm-3238")
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM.'], 404);
|
||||
}
|
||||
|
||||
if ((int) $vm->customer_id !== (int) $customer->id) {
|
||||
return response()->json(['success' => false, 'message' => 'Bạn không có quyền thao tác VM này.'], 403);
|
||||
}
|
||||
|
||||
return $vm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a ModuleVmware by ID, verify the authenticated user owns it.
|
||||
* Returns JsonResponse on error, ModuleVmware on success.
|
||||
* @deprecated Use resolveVmById instead for better ID format support
|
||||
*/
|
||||
private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse
|
||||
{
|
||||
return $this->resolveVmById($id, $request);
|
||||
}
|
||||
|
||||
/** Try to acquire a per-VM operation lock. Returns null if already locked. */
|
||||
private function acquireVmLock(ModuleVmware $vm, int $ttl = 60): ?\Illuminate\Contracts\Cache\Lock
|
||||
{
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Service;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\Service;
|
||||
use App\Services\ActivityLogger;
|
||||
use App\Services\InvoicePaymentService;
|
||||
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
|
||||
use App\Support\AddonQuantityValidator;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class VmRebuildController extends Controller
|
||||
@@ -103,7 +110,7 @@ class VmRebuildController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Không thể gửi yêu cầu rebuild: ' . $e->getMessage(),
|
||||
'message' => 'Không thể gửi yêu cầu rebuild, vui lòng thử lại sau.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
@@ -179,7 +186,9 @@ class VmRebuildController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function upgradeAddons(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
private const INVOICE_TYPE_UPGRADE = 3;
|
||||
|
||||
public function upgradeAddons(Request $request): JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
@@ -188,7 +197,7 @@ class VmRebuildController extends Controller
|
||||
'ram' => ['nullable', 'integer', 'min:0'],
|
||||
'disk' => ['nullable', 'integer', 'min:0'],
|
||||
'bandwidth' => ['nullable', 'integer', 'min:0'],
|
||||
'gpu' => ['nullable'],
|
||||
'gpu' => ['nullable', 'integer', 'min:0'],
|
||||
]);
|
||||
|
||||
$vm = $this->resolveVmById($payload['product'], $request);
|
||||
@@ -196,49 +205,160 @@ class VmRebuildController extends Controller
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$vm->loadMissing(['vCenter', 'ipAddress.host.datacenter.vCenter']);
|
||||
$vm->loadMissing(['order.orderItems.product']);
|
||||
|
||||
$vcenter = $vm->vCenter ?? $vm->ipAddress?->host?->datacenter?->vCenter;
|
||||
if (!$vcenter || !$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vCenter'], 400);
|
||||
$orderItem = $vm->order?->orderItems?->first();
|
||||
$product = $orderItem?->product;
|
||||
if (!$orderItem || !$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
|
||||
}
|
||||
|
||||
$applied = [];
|
||||
$service = Service::where('order_item_id', $orderItem->id)->first();
|
||||
if (!$service) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy dịch vụ hiện tại'], 404);
|
||||
}
|
||||
|
||||
if (isset($payload['cpu']) && $payload['cpu'] > 0) {
|
||||
$res = $serviceVCenter->UpgradeCPUVirtualMachine($vm->vmware_id, (int) $payload['cpu']);
|
||||
if (is_array($res) && !($res['error'] ?? true)) {
|
||||
$applied[] = 'cpu';
|
||||
}
|
||||
$requestedQtyByCode = array_filter([
|
||||
'cpu' => (int) ($payload['cpu'] ?? 0),
|
||||
'ram' => (int) ($payload['ram'] ?? 0),
|
||||
'disk' => (int) ($payload['disk'] ?? 0),
|
||||
'bandwidth' => (int) ($payload['bandwidth'] ?? 0),
|
||||
'gpu' => (int) ($payload['gpu'] ?? 0),
|
||||
], fn ($qty) => $qty > 0);
|
||||
|
||||
if (empty($requestedQtyByCode)) {
|
||||
return response()->json(['error' => true, 'message' => 'Vui lòng chọn ít nhất một addon để nâng cấp'], 422);
|
||||
}
|
||||
if (isset($payload['ram']) && $payload['ram'] > 0) {
|
||||
$res = $serviceVCenter->UpgradeMemoryVirtualMachine($vm->vmware_id, (int) $payload['ram']);
|
||||
if (is_array($res) && !($res['error'] ?? true)) {
|
||||
$applied[] = 'ram';
|
||||
|
||||
$activeAddons = $product->addons()->where('status', true)->get()->keyBy('id');
|
||||
$addonsByCode = $activeAddons->keyBy(fn ($addon) => strtolower((string) $addon->code));
|
||||
|
||||
$raw = [];
|
||||
foreach ($requestedQtyByCode as $code => $qty) {
|
||||
$addon = $addonsByCode->get($code);
|
||||
if (!$addon) {
|
||||
return response()->json(['error' => true, 'message' => "Addon \"{$code}\" không áp dụng cho sản phẩm này"], 422);
|
||||
}
|
||||
$raw[] = ['addon_id' => $addon->id, 'qty' => $qty];
|
||||
}
|
||||
if (isset($payload['disk']) && $payload['disk'] > 0) {
|
||||
$res = $serviceVCenter->UpgradeDiskVirtualMachine($vm, (int) $payload['disk']);
|
||||
if (is_array($res) && !($res['error'] ?? true)) {
|
||||
$applied[] = 'disk';
|
||||
}
|
||||
|
||||
$existingQtyByAddonId = collect(is_array($orderItem->addons_meta) ? $orderItem->addons_meta : [])
|
||||
->mapWithKeys(fn ($entry) => [(int) ($entry['addon_id'] ?? 0) => (int) ($entry['qty'] ?? 0)])
|
||||
->all();
|
||||
|
||||
try {
|
||||
$resolved = AddonQuantityValidator::resolve($raw, $activeAddons, $existingQtyByAddonId);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => collect($e->errors())->flatten()->first() ?? 'Yêu cầu nâng cấp không hợp lệ',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$existingInvoice = Invoice::where('service_id', $service->id)
|
||||
->where('invoice_type', self::INVOICE_TYPE_UPGRADE)
|
||||
->whereIn('status', InvoicePaymentService::PAYABLE_STATUSES)
|
||||
->first();
|
||||
|
||||
if ($existingInvoice) {
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Dịch vụ đã có hóa đơn nâng cấp đang chờ thanh toán.',
|
||||
'data' => $this->invoiceResponse($existingInvoice, $vm->order?->order_num),
|
||||
]);
|
||||
}
|
||||
|
||||
$cycleMonths = OrderHelper::cycleToMonths($orderItem->billing_cycle);
|
||||
$cycleDays = max(1, $cycleMonths * 30);
|
||||
$remainingDays = $service->expires_at && $service->expires_at->isFuture()
|
||||
? now()->diffInDays($service->expires_at)
|
||||
: 0;
|
||||
$remainingDays = (int) max(1, min($cycleDays, $remainingDays));
|
||||
|
||||
try {
|
||||
$invoice = DB::transaction(function () use ($service, $orderItem, $resolved, $remainingDays, $cycleDays) {
|
||||
$total = 0;
|
||||
$lines = [];
|
||||
|
||||
foreach ($resolved as $entry) {
|
||||
$unitPrice = $this->prorateUnitPrice((float) $entry['price'], $remainingDays, $cycleDays);
|
||||
$totalPrice = $unitPrice * $entry['qty'];
|
||||
$total += $totalPrice;
|
||||
|
||||
$lines[] = [
|
||||
'product_id' => $orderItem->product_id,
|
||||
'addon_id' => $entry['addon_id'],
|
||||
'type' => 'upgrade',
|
||||
'description' => 'Nâng cấp addon: ' . $entry['name'] . ' x' . $entry['qty'],
|
||||
'months' => 0,
|
||||
'quantity' => $entry['qty'],
|
||||
'unit_price' => $unitPrice,
|
||||
'total_price' => $totalPrice,
|
||||
];
|
||||
}
|
||||
|
||||
$invoice = Invoice::create([
|
||||
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
||||
'customer_id' => $service->customer_id,
|
||||
'order_id' => $orderItem->order_id,
|
||||
'service_id' => $service->id,
|
||||
'invoice_type' => self::INVOICE_TYPE_UPGRADE,
|
||||
'sub_total' => $total,
|
||||
'discount_amount' => 0,
|
||||
'tax' => 0,
|
||||
'total' => $total,
|
||||
'status' => InvoiceStatus::UNPAID->value,
|
||||
'due_date' => now()->addDays(3)->toDateString(),
|
||||
]);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
InvoiceItem::create($line + ['invoice_id' => $invoice->id]);
|
||||
}
|
||||
|
||||
return $invoice;
|
||||
});
|
||||
} catch (Throwable $e) {
|
||||
Log::error('VmRebuildController: upgradeAddons invoice creation failed', [
|
||||
'module_vmware_id' => $vm->id,
|
||||
'service_id' => $service->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json(['error' => true, 'message' => 'Không thể tạo yêu cầu nâng cấp, vui lòng thử lại sau.'], 500);
|
||||
}
|
||||
|
||||
$this->activityLogger->log(
|
||||
event: 'upgrade_addons',
|
||||
event: 'upgrade_addons_requested',
|
||||
logName: 'VM',
|
||||
description: "Customer {$customer->email} nâng cấp addons VM: {$vm->name} - áp dụng: " . implode(', ', $applied ?: ['none']),
|
||||
description: "Customer {$customer->email} yêu cầu nâng cấp addons VM: {$vm->name}, hóa đơn #{$invoice->invoice_number}",
|
||||
subject: $vm,
|
||||
newValues: array_filter(['cpu' => $payload['cpu'] ?? null, 'ram' => $payload['ram'] ?? null, 'disk' => $payload['disk'] ?? null]),
|
||||
newValues: ['invoice_id' => $invoice->id, 'addons' => $resolved],
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi yêu cầu nâng cấp addons',
|
||||
'data' => [
|
||||
'applied' => $applied,
|
||||
'note' => 'bandwidth/gpu hiện được ghi nhận yêu cầu, chưa có luồng tự động áp vào VM',
|
||||
],
|
||||
'message' => 'Đã tạo hóa đơn nâng cấp. Vui lòng thanh toán để áp dụng vào VM.',
|
||||
'data' => $this->invoiceResponse($invoice, $vm->order?->order_num),
|
||||
]);
|
||||
}
|
||||
|
||||
private function prorateUnitPrice(float $price, int $remainingDays, int $cycleDays): int
|
||||
{
|
||||
$raw = $price * $remainingDays / $cycleDays;
|
||||
|
||||
return (int) (ceil($raw / 1000) * 1000);
|
||||
}
|
||||
|
||||
private function invoiceResponse(Invoice $invoice, ?string $orderNum): array
|
||||
{
|
||||
return [
|
||||
'order_id' => $invoice->order_id,
|
||||
'order_num' => $orderNum,
|
||||
'invoice_id' => $invoice->id,
|
||||
'invoice_number' => $invoice->invoice_number,
|
||||
'total' => (float) $invoice->total,
|
||||
'status' => $invoice->status,
|
||||
'payment_url' => $orderNum ? '/hoa-don/' . $orderNum : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,18 @@ class ManualDepositController extends Controller
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (
|
||||
!str_starts_with($request->proof_image, "deposit-proofs/{$user->id}/")
|
||||
|| str_contains($request->proof_image, '..')
|
||||
|| !Storage::disk('local')->exists($request->proof_image)
|
||||
|| Transaction::where('proof_image', $request->proof_image)->exists()
|
||||
) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chứng từ không hợp lệ',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$transaction = Transaction::where('uuid', $request->uuid)
|
||||
->where('customer_id', $user->id)
|
||||
->where('type', 'manual')
|
||||
@@ -254,14 +266,15 @@ class ManualDepositController extends Controller
|
||||
public function uploadProof(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|image|mimes:jpeg,jpg,png|max:5120',
|
||||
'file' => 'required|image|mimes:jpeg,jpg,png,webp|max:5120',
|
||||
]);
|
||||
|
||||
$path = $request->file('file')->store('deposit-proofs', 'public');
|
||||
$user = $request->user();
|
||||
|
||||
$path = $request->file('file')->store('deposit-proofs/' . $user->id, 'local');
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'url' => Storage::disk('public')->url($path),
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
@@ -288,7 +301,7 @@ class ManualDepositController extends Controller
|
||||
'client_ip' => $request->ip(),
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'amount' => $transaction->amount,
|
||||
'proof_image_url' => $transaction->proof_image ? Storage::disk('public')->url($transaction->proof_image) : null,
|
||||
'proof_image_url' => $transaction->proof_image_urls[0] ?? null,
|
||||
'created_at' => now()->format('d/m/Y H:i:s'),
|
||||
]));
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -15,7 +15,7 @@ use App\Repositories\Module\IpAddress\IpAddressRepositoryInterface;
|
||||
use App\Repositories\Module\Network\NetworkRepositoryInterface;
|
||||
use App\Repositories\vCenter\vCenterRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Support\TlsHttpClientFactory;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
@@ -495,7 +495,7 @@ class VCenterManagerController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
$response = TlsHttpClientFactory::vCenter()
|
||||
->withHeaders([
|
||||
'vmware-api-session-id' => $vCenterService->token
|
||||
])
|
||||
@@ -555,7 +555,7 @@ class VCenterManagerController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
$response = TlsHttpClientFactory::vCenter()
|
||||
->withHeaders([
|
||||
'vmware-api-session-id' => $vCenterService->token
|
||||
])
|
||||
@@ -603,7 +603,7 @@ class VCenterManagerController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
$response = TlsHttpClientFactory::vCenter()
|
||||
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
|
||||
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmwareId/power/reset");
|
||||
|
||||
@@ -652,7 +652,7 @@ class VCenterManagerController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
$response = TlsHttpClientFactory::vCenter()
|
||||
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
|
||||
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmwareId/snapshot", [
|
||||
'spec' => [
|
||||
@@ -800,7 +800,7 @@ class VCenterManagerController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
$response = TlsHttpClientFactory::vCenter()
|
||||
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
|
||||
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmwareId/power/suspend");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user