diff --git a/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php b/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php index b5357fe..c49d7af 100644 --- a/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php +++ b/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php @@ -227,6 +227,8 @@ class VCenterController extends Controller 'uuid' => $vm->uuid, 'vm_id' => $vm->vm_id, 'ip' => $vm->ip, + 'username' => $vm->username, + 'password' => $vm->password, 'power_state' => $vm->power_state, 'folder' => $vm->folder, 'cpu' => $vm->cpu, diff --git a/app/Http/Controllers/Api/Service/VmController.php b/app/Http/Controllers/Api/Service/VmController.php index a834ac8..16cada2 100644 --- a/app/Http/Controllers/Api/Service/VmController.php +++ b/app/Http/Controllers/Api/Service/VmController.php @@ -18,6 +18,7 @@ use App\Services\ActivityLogger; use App\Services\VmProvisionService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; class VmController extends Controller @@ -48,7 +49,16 @@ class VmController extends Controller return $vm; } - $result = $action->execute($vm); + $lock = $this->acquireVmLock($vm); + if (!$lock) { + return $this->vmLocked(); + } + + try { + $result = $action->execute($vm); + } finally { + $lock->release(); + } $this->activityLogger->log( event: 'power_on', @@ -81,7 +91,16 @@ class VmController extends Controller return $vm; } - $result = $action->execute($vm); + $lock = $this->acquireVmLock($vm); + if (!$lock) { + return $this->vmLocked(); + } + + try { + $result = $action->execute($vm); + } finally { + $lock->release(); + } $this->activityLogger->log( event: 'power_off', @@ -149,15 +168,24 @@ class VmController extends Controller $dns = $validated['dns'] ?? '8.8.8.8'; - $raw = $action->execute( - $vm, - $validated['ip_address'], - $gateway, - $osType, - $dns, - $validated['guest_user'], - $validated['guest_password'] - ); + $lock = $this->acquireVmLock($vm, 120); + if (!$lock) { + return $this->vmLocked(); + } + + try { + $raw = $action->execute( + $vm, + $validated['ip_address'], + $gateway, + $osType, + $dns, + $validated['guest_user'], + $validated['guest_password'] + ); + } finally { + $lock->release(); + } // Always log the raw PowerShell output for debugging Log::channel('Custom')->info('[changeIp] raw output', [ @@ -244,7 +272,16 @@ class VmController extends Controller return $vm; } - $result = $action->execute($vm, (string) $validated['new_port']); + $lock = $this->acquireVmLock($vm); + if (!$lock) { + return $this->vmLocked(); + } + + try { + $result = $action->execute($vm, (string) $validated['new_port']); + } finally { + $lock->release(); + } // changeRDPPortWindows returns array on success/failure or string on login fail if ($result === 'Login_Again' || $result === 'Login_Fail') { @@ -334,7 +371,16 @@ class VmController extends Controller return $vm; } - $result = $serviceVCenter->restartVM($vm, $vm->vmware_id); + $lock = $this->acquireVmLock($vm); + if (!$lock) { + return $this->vmLocked(); + } + + try { + $result = $serviceVCenter->restartVM($vm, $vm->vmware_id); + } finally { + $lock->release(); + } if (is_string($result) || ($result['error'] ?? true)) { return response()->json([ @@ -352,7 +398,7 @@ class VmController extends Controller return response()->json([ 'error' => false, - 'message' => 'Đã gửi lệnh restart VM', + 'message' => 'Đang restart máy ảo vui lòng chờ!', 'data' => $result['data'] ?? 'success', ]); } @@ -380,6 +426,12 @@ class VmController extends Controller return $vm; } + // ponytail: don't release — lock expires after 5 min while the async job runs + $lock = $this->acquireVmLock($vm, 300); + if (!$lock) { + return $this->vmLocked(); + } + // Detect OS family from VM's os_id record $os = $vm->os_id ? Os::with('osGroup')->find($vm->os_id) : null; $osGroupName = strtolower($os?->osGroup?->name ?? ''); @@ -509,6 +561,21 @@ class VmController extends Controller 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 + { + $lock = Cache::lock("vm_op:{$vm->id}", $ttl); + return $lock->get() ? $lock : null; + } + + private function vmLocked(): JsonResponse + { + return response()->json([ + 'success' => false, + 'message' => 'VM đang có thao tác đang xử lý, vui lòng chờ rồi thử lại.', + ], 429); + } + /** * Normalise the array|string return value from Power ON/OFF actions. */ diff --git a/app/Mail/Customer/PasswordResetByAdmin.php b/app/Mail/Customer/PasswordResetByAdmin.php index 635480c..62293bd 100644 --- a/app/Mail/Customer/PasswordResetByAdmin.php +++ b/app/Mail/Customer/PasswordResetByAdmin.php @@ -35,7 +35,7 @@ class PasswordResetByAdmin extends Mailable 'fullName' => $data['fullname'] ?? 'Quý khách', 'email' => $data['email'] ?? '', 'password' => $data['password'] ?? '', - 'loginUrl' => $data['login_url'] ?? rtrim((string) env('FRONTEND_URL', ''), '/') . '/dang-nhap', + 'loginUrl' => $data['login_url'] ?? rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap', 'supportEmail' => $data['support_email'] ?? config('mail.support_email', 'support@thegioiso.com'), 'supportPhone' => $data['support_phone'] ?? config('mail.support_phone', '1900.xxxx'), ], @@ -60,7 +60,7 @@ class PasswordResetByAdmin extends Mailable 'company_name' => $settingValue['company'] ?? config('app.name', 'The Gioi So'), 'support_email' => $settingValue['email'] ?? config('mail.support_email', 'support@thegioiso.com'), 'support_phone' => $settingValue['hotline'] ?? ($settingValue['phone'] ?? config('mail.support_phone', '1900.xxxx')), - 'login_url' => rtrim((string) env('FRONTEND_URL', ''), '/') . '/dang-nhap', + 'login_url' => rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap', ], $this->data); } } diff --git a/app/Models/VirtualMachines.php b/app/Models/VirtualMachines.php index ea3645c..962f9f3 100644 --- a/app/Models/VirtualMachines.php +++ b/app/Models/VirtualMachines.php @@ -25,6 +25,8 @@ class VirtualMachines extends Model 'host_id', 'ip_pool_id', 'ip', + 'username', + 'password', ]; public function datacenter() diff --git a/config/app.php b/config/app.php index 33409ef..d350c10 100644 --- a/config/app.php +++ b/config/app.php @@ -57,6 +57,8 @@ return [ 'url' => env('APP_URL', 'http://localhost'), + 'frontend_url' => env('FRONTEND_URL', 'http://localhost'), + 'api_agency_url' => env('API_AGENCY_URL'), 'asset_url' => env('ASSET_URL', null), diff --git a/database/migrations/2026_07_14_000001_add_credentials_to_virtual_machines_table.php b/database/migrations/2026_07_14_000001_add_credentials_to_virtual_machines_table.php new file mode 100644 index 0000000..c7359d3 --- /dev/null +++ b/database/migrations/2026_07_14_000001_add_credentials_to_virtual_machines_table.php @@ -0,0 +1,23 @@ +string('username')->nullable()->after('ip'); + $table->string('password')->nullable()->after('username'); + }); + } + + public function down(): void + { + Schema::table('virtual_machines', function (Blueprint $table) { + $table->dropColumn(['username', 'password']); + }); + } +}; diff --git a/resources/views/emails/customer/password-reset-admin.blade.php b/resources/views/emails/customer/password-reset-admin.blade.php index 5bfb40d..dfdda04 100644 --- a/resources/views/emails/customer/password-reset-admin.blade.php +++ b/resources/views/emails/customer/password-reset-admin.blade.php @@ -3,7 +3,7 @@ $fullName = $fullName ?? 'Quý khách'; $email = $email ?? ''; $password = $password ?? ''; - $loginUrl = $loginUrl ?? rtrim((string) env('FRONTEND_URL', ''), '/') . '/dang-nhap'; + $loginUrl = $loginUrl ?? rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap'; $supportEmail = $supportEmail ?? 'support@tgs.com.vn'; $supportPhone = $supportPhone ?? '1900. 6119'; @endphp diff --git a/resources/views/emails/customer/welcome.blade.php b/resources/views/emails/customer/welcome.blade.php index 04255ea..90acecf 100644 --- a/resources/views/emails/customer/welcome.blade.php +++ b/resources/views/emails/customer/welcome.blade.php @@ -1,7 +1,7 @@ @php $appName = $appName ?? config('app.name', 'Thế Giới Số'); $fullName = $fullName ?? 'Quý khách'; - $loginUrl = $loginUrl ?? rtrim((string) env('FRONTEND_URL', ''), '/') . '/dang-nhap'; + $loginUrl = $loginUrl ?? rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap'; $supportEmail = $supportEmail ?? 'support@tgs.com.vn'; $supportPhone = $supportPhone ?? '1900. 6119'; @endphp