diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8c32391..1e770b2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -44,7 +44,8 @@ "Bash(python3 *)", "Bash(pnpm exec *)", "Bash(echo \"exit: $?\")", - "Bash(node -e \"console.log\\(require.resolve\\('ag-grid-react/package.json'\\)\\)\")" + "Bash(node -e \"console.log\\(require.resolve\\('ag-grid-react/package.json'\\)\\)\")", + "Bash(awk 'NR==1,NR==900' routes/api.php)" ] } } diff --git a/.env.example b/.env.example index b54a59a..4561fab 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,9 @@ RSA_PRIVATE="-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQDhm9x8DFf8D4pVBgPi6N PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1IW8ZeL+cwbatI1sQwt2zEmWwI2ovityTTz3/O+WGXgD85Zz\nEOA0thy91mJMNZv5qyWPexbmROrYpUSe9mQl4HIDlMipuWE+pJImmrJGGCf15QdO\nOVQvoknwVxr3lfSGwEFx2ubPMb1asytawbzQ/pZc6F7VP8gqcdVpI0JnolDtXa/v\niaURTIvYY1WtwPjRa4m98fin4aKMh3sDt88W00RaMjPn00aW8NRo8s/Oa0d/DXEI\ne08RGGUBHuuqRhzB4wHYnpafBqpZDp2LYHvdw+7Vf4dPh63jfBy6ISv+eGSIFuSE\npuiumDt4J7ZjCjh+RDavJQhfGGiG+kzMZ4iAZwIDAQABAoIBACME0nF7gPAVTg54\nS0GU7wNkkNmzvf1wDUqa5EYieHpD42rM/LLoLudfPwzHb8VZqT9BVi4zLMhoZkB9\nWTBCeldMkjh/BDCOzJKznnbZL2GlnAgcNxCNRbgZNOg1glyMSXP+lLM8WyHYEMz7\nNN49rbZ1smN71OnIou3U+5kaxRbir8RNh93fp5yt519a7gRybL8Td2FH64lcSRuX\nHVDtwX95d9m8tY8WoxQPz7jzztUlfNWgRgQhBSYXpOjAEWoJaXnOYo2mxoR6pm9p\npqgkgGvwmfv0Mi97anFoPMymM3HzUFOqqtr5Q7M6DXC+1CSmtkw6hvpED/IV6RJh\nOQtG6hECgYEA/fdnE6m7NMJe4jZxq+mB7+4HYvHLodl8Vb0ncIHwbhAiqaoQNUj2\nfzotHdBeligiives4S3N10RwvKNb1M0iJs41GmfTLcJRoUvyfHbYWLX+M5FRdQ4q\nvihFj8ckontcdXUSoqdZXz0KcVMkHq1hnXt7CayfvXwTeFDdfPoevUkCgYEA1jlg\n6pYg3xGAGNPF5FGiVEMp+BT7h6w41fRmL7c7LFkksUkXmbOIY+M/NLzx2mNUUXvL\nOfMjFUtjr2kehApS6/v7pgJjqqcmFYQJAMnwsVmuBVDw7+OCxK7IrR24jwYLpVMd\nzOiWALZMlLD1bnP0v6sa0hb5/ag8bFElpV+FwC8CgYEAromI3YqYjfnHG9Au7ubb\n1fjQOr/SIrlFKeeFY1zpz7pnGBM4cGZkgauJtiOt3aRXmQn+VgMUpAz1La9XPTh9\ncA3nkO4pxBj34NJBJCjJJq5stSP5x2ilF2q148oI82G0eUSsOfyo/zsEIj3xZOPj\nYkeql0t7vM2408c/cNS0q4ECgYEArDX8HxkI28npvAuiLeUiJbLIblIP0gU0u2p5\nKyqVOxV+VbZ4bmrQ3du01+R8WB85jTeDK9a1AU1VmZFborHZ7zLmHZIWJP2xs+Is\niqHMCAYbytJCndW7LfmEAQa/ctpdSk4CytK+o1vpPC71XhGBUwNt0C4AUVWJHVou\ndT7Q+qkCgYAE0QGNls/EUcCFoX6LUXDAQZFe46rp5hxcGOrjdjbgZl5DiJSrnEvH\n1sh908Q1FchJagA+nUVOm+nbOV3EMf6XlHRyB9XtRcSgEfls9O0HWUi/67YJig9L\ncElLxwyrZpLdzMRYz0JknXSb7GnnsZPG4HG5rhMMz/Ly7lb6f7GZ1w==\n-----END RSA PRIVATE KEY-----" PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1IW8ZeL+cwbatI1sQwt2\nzEmWwI2ovityTTz3/O+WGXgD85ZzEOA0thy91mJMNZv5qyWPexbmROrYpUSe9mQl\n4HIDlMipuWE+pJImmrJGGCf15QdOOVQvoknwVxr3lfSGwEFx2ubPMb1asytawbzQ\n/pZc6F7VP8gqcdVpI0JnolDtXa/viaURTIvYY1WtwPjRa4m98fin4aKMh3sDt88W\n00RaMjPn00aW8NRo8s/Oa0d/DXEIe08RGGUBHuuqRhzB4wHYnpafBqpZDp2LYHvd\nw+7Vf4dPh63jfBy6ISv+eGSIFuSEpuiumDt4J7ZjCjh+RDavJQhfGGiG+kzMZ4iA\nZwIDAQAB\n-----END PUBLIC KEY-----" +#Config VMware template default credential +VMWARE_TEMPLATE_PASSWORD=change-me + #CONFIG CKFINDER CKFINDER_DOMAIN=client.test CKFINDER_LICENSE=WS8SXEQQXPN8MYTF51SX1VFFUW3B6 diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index ca2e484..0624ac5 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -6,12 +6,14 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Api\Client\ClientAuthRequest; use App\Http\Requests\Api\Client\ClientChangePasswordRequest; use App\Http\Requests\Api\Client\ClientRegisterRequest; +use App\Http\Requests\Api\Client\ClientUpdateCccdRequest; use App\Http\Requests\Api\Client\ClientUpdateRequest; use App\Http\Resources\Api\Client\ClientResource; use App\Services\ActivityLogger; use App\Services\Api\Client\ClientAuthService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Storage; use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Exceptions\JWTException; @@ -467,6 +469,43 @@ class ClientController extends Controller ]); } + public function updateCccd(ClientUpdateCccdRequest $request) + { + $user = auth('client')->user(); + + $oldValues = array_intersect_key($user->toArray(), $request->validated()); + + $user->identity_number = $request->validated('cmnd'); + $user->front_identity_number = Storage::disk('public')->url( + $request->file('cmnd_before')->store('cccd', 'public') + ); + $user->back_identity_number = Storage::disk('public')->url( + $request->file('cmnd_after')->store('cccd', 'public') + ); + + $dirty = $user->getDirty(); + $user->save(); + + if (!empty($dirty)) { + $this->activityLogger->log( + event: 'cccd_update', + logName: 'Customer', + description: "Customer {$user->email} cập nhật chứng minh thư", + subject: $user, + oldValues: array_intersect_key($oldValues, $dirty), + newValues: $dirty, + ); + } + + return response()->json([ + 'error' => false, + 'message' => 'Cập nhật chứng minh thư thành công', + 'data' => [ + 'client' => new ClientResource($user->loadCount(['vps', 'hosting', 'domain'])), + ], + ]); + } + public function changePassword(ClientChangePasswordRequest $request) { $user = auth('client')->user(); diff --git a/app/Http/Controllers/Api/Client/Ticket/TicketController.php b/app/Http/Controllers/Api/Client/Ticket/TicketController.php index 527dee5..3631ef5 100644 --- a/app/Http/Controllers/Api/Client/Ticket/TicketController.php +++ b/app/Http/Controllers/Api/Client/Ticket/TicketController.php @@ -103,7 +103,7 @@ class TicketController extends Controller return response()->json([ 'error' => false, - 'message' => 'Ticket đã được tạo thành công.', + 'message' => 'Ticket đã được khởi tạo.', 'data' => new TicketResource($ticket), ], 201); } diff --git a/app/Http/Requests/Api/Client/ClientUpdateCccdRequest.php b/app/Http/Requests/Api/Client/ClientUpdateCccdRequest.php new file mode 100644 index 0000000..4ebf6d1 --- /dev/null +++ b/app/Http/Requests/Api/Client/ClientUpdateCccdRequest.php @@ -0,0 +1,33 @@ +check(); + } + + public function rules(): array + { + return [ + 'cmnd' => 'required|string|max:50', + 'cmnd_before' => 'required|image|max:5120', + 'cmnd_after' => 'required|image|max:5120', + ]; + } + + public function messages(): array + { + return [ + 'cmnd.required' => 'Số CMT/CCCD là bắt buộc', + 'cmnd_before.required' => 'Ảnh mặt trước là bắt buộc', + 'cmnd_before.image' => 'Ảnh mặt trước phải là hình ảnh', + 'cmnd_after.required' => 'Ảnh mặt sau là bắt buộc', + 'cmnd_after.image' => 'Ảnh mặt sau phải là hình ảnh', + ]; + } +} diff --git a/app/Http/Resources/Api/Client/ClientResource.php b/app/Http/Resources/Api/Client/ClientResource.php index 5720498..91d0d11 100644 --- a/app/Http/Resources/Api/Client/ClientResource.php +++ b/app/Http/Resources/Api/Client/ClientResource.php @@ -31,8 +31,8 @@ class ClientResource extends JsonResource 'date' => $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : null, 'company' => $this->company ?? '', 'avatar' => $this->avatar, - 'cmnd_after' => $this->front_identity_number, - 'cmnd_before' => $this->back_identity_number, + 'cmnd_before' => $this->front_identity_number, + 'cmnd_after' => $this->back_identity_number, 'cmnd' => $this->identity_number, 'fb_link' => null, 'active_2fa' => 0, diff --git a/app/Http/Resources/Api/Service/Order/InvoiceDetailResource.php b/app/Http/Resources/Api/Service/Order/InvoiceDetailResource.php index 6265a62..d4e8881 100644 --- a/app/Http/Resources/Api/Service/Order/InvoiceDetailResource.php +++ b/app/Http/Resources/Api/Service/Order/InvoiceDetailResource.php @@ -8,6 +8,8 @@ use Illuminate\Http\Resources\Json\JsonResource; class InvoiceDetailResource extends JsonResource { + use ResolvesPaymentMethod; + public function toArray(Request $request): array { return [ @@ -20,7 +22,7 @@ class InvoiceDetailResource extends JsonResource 'discount_amount' => (float) ($this->discount_amount ?? 0), 'tax' => (float) $this->tax, 'total' => (float) $this->total, - 'payment_method' => $this->payment_method, + 'payment_method' => $this->resolvePaymentMethod($this->resource), 'auto_renew' => (bool) $this->auto_renew, 'created_at' => $this->created_at?->toISOString(), 'due_date' => $this->due_date?->toISOString(), diff --git a/app/Http/Resources/Api/Service/Order/OrderDetailResource.php b/app/Http/Resources/Api/Service/Order/OrderDetailResource.php index b53a152..8d37b8f 100644 --- a/app/Http/Resources/Api/Service/Order/OrderDetailResource.php +++ b/app/Http/Resources/Api/Service/Order/OrderDetailResource.php @@ -9,6 +9,8 @@ use Illuminate\Http\Resources\Json\JsonResource; class OrderDetailResource extends JsonResource { + use ResolvesPaymentMethod; + public function toArray(Request $request): array { return [ @@ -33,6 +35,7 @@ class OrderDetailResource extends JsonResource 'total' => (float) $this->invoice->total, 'due_date' => $this->invoice->due_date?->toISOString(), 'paid_at' => $this->invoice->paid_at?->toISOString(), + 'payment_method' => $this->resolvePaymentMethod($this->invoice), ]), 'items' => $this->whenLoaded('orderItems') ? OrderItemResource::collection($this->orderItems) : [], ]; diff --git a/app/Http/Resources/Api/Service/Order/ResolvesPaymentMethod.php b/app/Http/Resources/Api/Service/Order/ResolvesPaymentMethod.php new file mode 100644 index 0000000..396ffa0 --- /dev/null +++ b/app/Http/Resources/Api/Service/Order/ResolvesPaymentMethod.php @@ -0,0 +1,43 @@ + 'Số dư tài khoản', + 'credit' => 'Số dư tài khoản', + 'transfer' => 'Chuyển khoản ngân hàng', + 'vnpay' => 'VNPay', + 'momo' => 'Momo', + 'bank_transfer' => 'Chuyển khoản ngân hàng', + 'admin' => 'Xác nhận thủ công', + ]; + + private function resolvePaymentMethod(Invoice $invoice): ?array + { + $method = $invoice->payment_method; + if (! $method) { + return null; + } + + if (ctype_digit((string) $method) && $invoice->payment) { + return [ + 'code' => null, + 'name' => $invoice->payment->name, + 'bank_name' => $invoice->payment->bankName, + 'logo' => $invoice->payment->logo, + ]; + } + + return [ + 'code' => $method, + 'name' => self::METHOD_LABELS[$method] ?? $method, + 'bank_name' => null, + 'logo' => null, + ]; + } +} diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index e9c9d7f..f06dd4a 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -343,12 +343,41 @@ class vCenter // ─── Legacy VPS Direct SSH ──────────────────────────────────────────────── // These methods use direct SSH to guest VM, not via jumphost. + /** Look up host/user/password for a legacy target from infra_management, decrypting the stored password. */ + private function resolveLegacyInfra(string $type): ?array + { + $infra = \App\Models\InfraManagement::where('type', $type) + ->where('status', true) + ->first(); + + if (!$infra) { + Log::error("Legacy VPS lookup failed: no active infra_management row for type={$type}."); + return null; + } + + return [ + 'ip' => $infra->host, + 'user' => $infra->username, + 'password' => \App\Http\Facade\SignRSA::DecryptFunc($infra->password), + ]; + } + protected function loginVPS(array|null $data = []): ?SSH2 { try { - $user = $data['user'] ?? 'root'; - $ip = $data['ip'] ?? '103.42.58.37'; - $password = $data['password'] ?? 'TGS@1232'; + $ip = $data['ip'] ?? null; + $user = $data['user'] ?? null; + $password = $data['password'] ?? null; + + if ($ip === null || $user === null || $password === null) { + $infra = $this->resolveLegacyInfra('legacy_vps'); + if (!$infra) { + throw new Exception('No legacy VPS credentials available (data not provided and no active infra_management row).'); + } + $ip ??= $infra['ip']; + $user ??= $infra['user']; + $password ??= $infra['password']; + } $this->process = new SSH2($ip, 22, 10); if (!$this->process->login($user, $password)) { @@ -362,13 +391,19 @@ class vCenter } } - public function change_password_linux( - array $data = ['ip' => '10.10.10.173', 'user' => 'admin', 'password' => 'TGS@123', 'new_password' => ''], - ): bool { + public function change_password_linux(array $data = ['new_password' => '']): bool + { try { if (empty($data['new_password'])) { throw new Exception('New password cannot be empty.'); } + if (!isset($data['ip'], $data['user'], $data['password'])) { + $infra = $this->resolveLegacyInfra('jumphost'); + if (!$infra) { + throw new Exception('No jumphost credentials available in infra_management.'); + } + $data += $infra; + } $this->loginVPS($data); if (!$this->process) { throw new Exception('Cannot login to VPS.'); @@ -383,18 +418,19 @@ class vCenter } } - public function change_password_windows( - array $data = [ - 'ip' => '103.42.58.37', - 'user' => 'administrator', - 'password' => 'TGS@123', - 'new_password' => '', - ], - ): bool { + public function change_password_windows(array $data = ['new_password' => '']): bool + { try { if (empty($data['new_password'])) { throw new Exception('New password cannot be empty.'); } + if (!isset($data['ip'], $data['user'], $data['password'])) { + $infra = $this->resolveLegacyInfra('legacy_vps'); + if (!$infra) { + throw new Exception('No legacy VPS credentials available in infra_management.'); + } + $data += $infra; + } $this->loginVPS($data); if (!$this->process) { throw new Exception('Cannot login to VPS.'); diff --git a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php index ddac7f7..c4493cf 100644 --- a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php +++ b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php @@ -61,7 +61,7 @@ class AssignIpToGuestJob implements ShouldQueue public readonly int $moduleVmwareId, public readonly string $vmId, public readonly string $osFamily, // VmProvisionService::OS_LINUX | OS_WINDOWS - public readonly string $guestPassword = VmProvisionService::TEMPLATE_PASSWORD, + public readonly string $guestPassword, public readonly ?int $reservationId = null, public readonly ?int $reservedIpPoolId = null, ) {} diff --git a/app/Jobs/VirtualMachine/Provision/RetryProvisioningOrchestrator.php b/app/Jobs/VirtualMachine/Provision/RetryProvisioningOrchestrator.php index 98e1540..a62f5e3 100644 --- a/app/Jobs/VirtualMachine/Provision/RetryProvisioningOrchestrator.php +++ b/app/Jobs/VirtualMachine/Provision/RetryProvisioningOrchestrator.php @@ -157,7 +157,7 @@ class RetryProvisioningOrchestrator implements ShouldQueue moduleVmwareId: $moduleVmware->id, vmId: $vmId, osFamily: $osFamily, - guestPassword: $moduleVmware->plain_password ?? \App\Services\VmProvisionService::TEMPLATE_PASSWORD, + guestPassword: $moduleVmware->plain_password ?? \App\Services\VmProvisionService::templatePassword(), ); Log::info('RetryProvisioningOrchestrator: dispatched AssignIpToGuestJob', [ 'module_vmware_id' => $moduleVmware->id, @@ -301,8 +301,8 @@ class RetryProvisioningOrchestrator implements ShouldQueue 'next_due_date' => $nextDueDate, 'next_invoice' => $nextDueDate, 'user' => 'Administrator', - 'password' => \App\Services\VmProvisionService::TEMPLATE_PASSWORD, - 'plain_password' => \App\Services\VmProvisionService::TEMPLATE_PASSWORD, + 'password' => \App\Services\VmProvisionService::templatePassword(), + 'plain_password' => \App\Services\VmProvisionService::templatePassword(), 'hostname' => $serverName, 'vmware_id' => $vmId, 'vcenter_id' => $vcenterModel->id, diff --git a/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php b/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php index 3f15477..f694206 100644 --- a/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php +++ b/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php @@ -368,7 +368,7 @@ class UpdateVmConfigJob implements ShouldQueue $this->moduleVmwareId, $this->vmId, $this->osFamily, - VmProvisionService::TEMPLATE_PASSWORD, + VmProvisionService::templatePassword(), $this->reservationId, $this->reservedIpPoolId, ); diff --git a/app/Models/ModuleVmware.php b/app/Models/ModuleVmware.php index 38c7abc..39dc86d 100644 --- a/app/Models/ModuleVmware.php +++ b/app/Models/ModuleVmware.php @@ -41,7 +41,11 @@ class ModuleVmware extends Model // NOTE: password must NOT use 'hashed' cast — it is a VM guest credential // used in plaintext by PowerShell scripts (Invoke-VMScript) via SSH/PowerCLI. - protected $casts = []; + // plain_password uses Laravel's 'encrypted' cast (APP_KEY, AES-256-CBC): stored + // encrypted at rest, transparently decrypted on read — no code elsewhere changes. + protected $casts = [ + 'plain_password' => 'encrypted', + ]; public function customer() { return $this->belongsTo(\App\Models\Customer::class); diff --git a/app/Services/Api/Order/OrderQueryService.php b/app/Services/Api/Order/OrderQueryService.php index 9f1864c..d0bbac4 100644 --- a/app/Services/Api/Order/OrderQueryService.php +++ b/app/Services/Api/Order/OrderQueryService.php @@ -105,6 +105,7 @@ class OrderQueryService 'invoice.invoiceItems', 'invoice.attachments', 'invoice.customer', + 'invoice.payment', 'orderItems.product', 'customer', 'vmWare.ipAddress', diff --git a/app/Services/VirtualMachine/Rebuild/VmRebuildIpService.php b/app/Services/VirtualMachine/Rebuild/VmRebuildIpService.php index d1ffee0..b5b6050 100644 --- a/app/Services/VirtualMachine/Rebuild/VmRebuildIpService.php +++ b/app/Services/VirtualMachine/Rebuild/VmRebuildIpService.php @@ -132,7 +132,7 @@ final class VmRebuildIpService vcenterHost: $vcenterHost, vcenterUser: $vcenterUser, vcenterPassword: $vcenterPass, - guestPassword: VmRebuildService::TEMPLATE_PASSWORD, + guestPassword: VmRebuildService::templatePassword(), osFamily: $ctx->osFamily, dns: $dns, guestUser: $guestUser, @@ -174,7 +174,7 @@ final class VmRebuildIpService vcenterHost: $vcenterHost, vcenterUser: $vcenterUser, vcenterPassword: $vcenterPass, - guestPassword: VmRebuildService::TEMPLATE_PASSWORD, + guestPassword: VmRebuildService::templatePassword(), osFamily: $ctx->osFamily, guestUser: $guestUser, ); diff --git a/app/Services/VirtualMachine/Rebuild/VmRebuildService.php b/app/Services/VirtualMachine/Rebuild/VmRebuildService.php index ba88fda..8b16b40 100644 --- a/app/Services/VirtualMachine/Rebuild/VmRebuildService.php +++ b/app/Services/VirtualMachine/Rebuild/VmRebuildService.php @@ -35,12 +35,11 @@ final class VmRebuildService public const OS_LINUX = 'LINUX'; public const OS_WINDOWS = 'WINDOWS'; - /** - * Default password baked into rebuild templates. Mirrors the create-flow - * template password constant but is duplicated intentionally to keep this - * subsystem standalone. - */ - public const TEMPLATE_PASSWORD = 'TGS@123'; + /** Default password baked into rebuild templates — shares config with the create flow. */ + public static function templatePassword(): string + { + return \App\Services\VmProvisionService::templatePassword(); + } /** * Build a fresh RebuildContext from current DB state. diff --git a/app/Services/VirtualMachine/VmModuleService.php b/app/Services/VirtualMachine/VmModuleService.php index 7eddf7e..956bcfd 100644 --- a/app/Services/VirtualMachine/VmModuleService.php +++ b/app/Services/VirtualMachine/VmModuleService.php @@ -35,8 +35,8 @@ class VmModuleService 'next_due_date' => $nextDueDate, 'next_invoice' => $nextDueDate, 'user' => $infra->vmUser, - 'password' => VmProvisionService::TEMPLATE_PASSWORD, - 'plain_password' => VmProvisionService::TEMPLATE_PASSWORD, + 'password' => VmProvisionService::templatePassword(), + 'plain_password' => VmProvisionService::templatePassword(), 'hostname' => $serverName, 'vmware_id' => $vmId, 'vcenter_id' => $infra->vcenter->id, diff --git a/app/Services/VmProvisionService.php b/app/Services/VmProvisionService.php index c758198..1b91e07 100644 --- a/app/Services/VmProvisionService.php +++ b/app/Services/VmProvisionService.php @@ -12,12 +12,15 @@ use Illuminate\Support\Facades\Log; final class VmProvisionService { - /** Default password baked into every VM template */ - public const TEMPLATE_PASSWORD = 'TGS@123'; - public const OS_LINUX = 'LINUX'; public const OS_WINDOWS = 'WINDOWS'; + /** Default password baked into every VM template */ + public static function templatePassword(): string + { + return config('vmware.template_password'); + } + // ------------------------------------------------------------------------- // vCenter REST helpers (used by WaitVmReadyJob if still in flow) // ------------------------------------------------------------------------- diff --git a/config/vmware.php b/config/vmware.php index 58cbc44..ae35ca3 100644 --- a/config/vmware.php +++ b/config/vmware.php @@ -1,6 +1,9 @@ env('VMWARE_TEMPLATE_PASSWORD', 'TGS@123'), + // HAProxy (console.tgs.com.vn) routes WMKS console websockets to the // ESXi host that owns the ticket via the `?host=` query param — see // ha-proxy.md. This maps each ESXi IP to the domain HAProxy matches on. diff --git a/database/migrations/2026_08_07_000000_encrypt_module_vmware_plain_password.php b/database/migrations/2026_08_07_000000_encrypt_module_vmware_plain_password.php new file mode 100644 index 0000000..6d128ca --- /dev/null +++ b/database/migrations/2026_08_07_000000_encrypt_module_vmware_plain_password.php @@ -0,0 +1,52 @@ +whereNotNull('plain_password') + ->where('plain_password', '!=', '') + ->orderBy('id') + ->chunkById(200, function ($rows) { + foreach ($rows as $row) { + // Skip rows already encrypted (re-running the migration, or a value + // that happens to look like ciphertext) — avoid double-encrypting. + try { + Crypt::decryptString($row->plain_password); + continue; + } catch (\Throwable) { + // not yet encrypted — fall through and encrypt it + } + + DB::table('module_vmware') + ->where('id', $row->id) + ->update(['plain_password' => Crypt::encryptString($row->plain_password)]); + } + }); + } + + public function down(): void + { + DB::table('module_vmware') + ->whereNotNull('plain_password') + ->where('plain_password', '!=', '') + ->orderBy('id') + ->chunkById(200, function ($rows) { + foreach ($rows as $row) { + try { + $plain = Crypt::decryptString($row->plain_password); + } catch (\Throwable) { + continue; + } + + DB::table('module_vmware') + ->where('id', $row->id) + ->update(['plain_password' => $plain]); + } + }); + } +}; diff --git a/database/migrations/2026_08_07_000001_seed_legacy_vps_infra_management.php b/database/migrations/2026_08_07_000001_seed_legacy_vps_infra_management.php new file mode 100644 index 0000000..5bebe73 --- /dev/null +++ b/database/migrations/2026_08_07_000001_seed_legacy_vps_infra_management.php @@ -0,0 +1,28 @@ + '103.42.58.37'], + [ + 'name' => 'Legacy VPS', + 'username' => 'administrator', + 'password' => SignRSA::EncryptFunc('TGS@123'), + 'type' => 'legacy_vps', + 'status' => true, + 'meta_data' => ['port' => 22], + 'description' => 'Legacy direct-SSH-to-guest target (vCenter::loginVPS / change_password_windows fallback)', + ], + ); + } + + public function down(): void + { + InfraManagement::where('host', '103.42.58.37')->where('type', 'legacy_vps')->delete(); + } +}; diff --git a/database/seeders/InfraManagementSeeder.php b/database/seeders/InfraManagementSeeder.php deleted file mode 100644 index fd5c48f..0000000 --- a/database/seeders/InfraManagementSeeder.php +++ /dev/null @@ -1,31 +0,0 @@ - '10.10.10.173'], - [ - 'name' => 'Jumphost 173', - 'username' => 'administrator', - 'password' => SignRSA::EncryptFunc('Thegioiso@123**$$8899TgTk2'), - 'type' => 'jumphost', - 'status' => true, - 'meta_data' => [ - 'port' => 22 - ], - 'description' => 'Main Jumphost for vCenter PowerCLI operations' - ] - ); - } -} diff --git a/database/seeders/IpPoolSeeder.php b/database/seeders/IpPoolSeeder.php deleted file mode 100644 index a834022..0000000 --- a/database/seeders/IpPoolSeeder.php +++ /dev/null @@ -1,227 +0,0 @@ - 'LAN-VPS-01', 'cidr' => '103.90.224.0/26', 'gateway' => '103.90.224.1'] - * - * // Theo range: - * ['network' => 'LAN-VPS-01', 'start' => '103.90.224.10', 'end' => '103.90.224.60'] - * - * // Danh sách IP thủ công: - * ['network' => 'LAN-VPS-01', 'ips' => ['103.90.224.10', '103.90.224.11', ...]] - * - * // Có thể dùng module_network_id thay vì tên network: - * ['network_id' => 1, 'cidr' => '103.90.224.0/26'] - * - * IP đầu (network address), IP cuối (broadcast), và gateway trong CIDR tự động bị loại bỏ. - * Khi VM được tạo, hệ thống sẽ lấy IP đầu tiên chưa được assign trong module_ip_address. - */ -class IpPoolSeeder extends Seeder -{ - /** - * ============================================================ - * ĐỊA CHỈ IP POOLS — CHỈNH SỬA TẠI ĐÂY - * ============================================================ - */ - private function pools(): array - { - return [ - // --- Pool 1: VPS Public Network (module_network id=1, name="VM Network") --- - [ - 'network_id' => 1, // module_network.id - 'cidr' => '103.90.224.0/26', // /26, gateway .1, cấp phát .2 → .62 - 'gateway' => '103.90.224.1', - ], - - // --- Pool 2: thêm range thủ công --- - // [ - // 'network' => 'LAN-VPS-02', - // 'start' => '10.10.20.50', - // 'end' => '10.10.20.100', - // ], - - // --- Pool 3: danh sách IP cụ thể --- - // [ - // 'network_id' => 2, - // 'ips' => [ - // '192.168.100.10', - // '192.168.100.11', - // '192.168.100.12', - // ], - // ], - ]; - } - - // ============================================================ - - public function run(): void - { - $totalInserted = 0; - $totalSkipped = 0; - - foreach ($this->pools() as $pool) { - $network = $this->resolveNetwork($pool); - - if (!$network) { - $this->command->warn('IpPoolSeeder: không tìm thấy network: ' . json_encode($pool)); - continue; - } - - $ips = $this->generateIps($pool); - - if (empty($ips)) { - $this->command->warn("IpPoolSeeder: không có IP nào được generate cho network #{$network->id}"); - continue; - } - - foreach ($ips as $ip) { - $created = IpPool::updateOrCreate( - [ - 'ip_address' => $ip, - 'module_network_id' => $network->id, - ], - [ - 'gateway' => $pool['gateway'] ?? null, - 'cidr' => $this->resolveCidr($pool), - ] - ); - - if ($created->wasRecentlyCreated) { - $totalInserted++; - } else { - $totalSkipped++; - } - } - - $this->command->info("✅ Network [{$network->name}] (id={$network->id}): " . count($ips) . " IPs xử lý xong."); - } - - $this->command->info("IpPoolSeeder hoàn tất: {$totalInserted} IPs mới, {$totalSkipped} IPs đã tồn tại."); - } - - // ------------------------------------------------------------------------- - - private function resolveNetwork(array $pool): ?ModuleNetwork - { - if (!empty($pool['network_id'])) { - return ModuleNetwork::find($pool['network_id']); - } - - if (!empty($pool['network'])) { - return ModuleNetwork::where('name', $pool['network'])->first(); - } - - return null; - } - - /** - * Generate danh sách IP từ CIDR, range start–end, hoặc mảng IP thủ công. - * - * @return string[] - */ - private function generateIps(array $pool): array - { - if (!empty($pool['ips'])) { - return $this->withoutGateway( - array_values(array_filter($pool['ips'], fn($ip) => filter_var($ip, FILTER_VALIDATE_IP))), - $pool - ); - } - - if (!empty($pool['cidr'])) { - return $this->withoutGateway($this->ipsFromCidr($pool['cidr']), $pool); - } - - if (!empty($pool['start']) && !empty($pool['end'])) { - return $this->withoutGateway($this->ipsFromRange($pool['start'], $pool['end']), $pool); - } - - return []; - } - - private function resolveCidr(array $pool): int - { - if (!empty($pool['prefix'])) { - return (int) $pool['prefix']; - } - - if (!empty($pool['cidr']) && str_contains($pool['cidr'], '/')) { - return (int) explode('/', $pool['cidr'])[1]; - } - - return 24; - } - - /** - * Không đưa gateway vào IP pool để tránh cấp gateway cho VM. - * - * @param string[] $ips - * @return string[] - */ - private function withoutGateway(array $ips, array $pool): array - { - $gateway = $pool['gateway'] ?? null; - - if (!$gateway || !filter_var($gateway, FILTER_VALIDATE_IP)) { - return $ips; - } - - return array_values(array_filter($ips, fn(string $ip): bool => $ip !== $gateway)); - } - - /** - * Lấy tất cả usable IPs từ CIDR (bỏ network address và broadcast). - * Ví dụ: 103.90.224.0/26 → .1 đến .62 - */ - private function ipsFromCidr(string $cidr): array - { - [$baseIp, $prefix] = explode('/', $cidr); - - $prefix = (int) $prefix; - $baseInt = ip2long($baseIp); - $mask = ~((1 << (32 - $prefix)) - 1); - $network = $baseInt & $mask; - $broadcast = $network | ~$mask; - - $ips = []; - // Bỏ qua network address (.0) và broadcast (.255 hoặc cuối range) - for ($i = $network + 1; $i < $broadcast; $i++) { - $ips[] = long2ip($i); - } - - return $ips; - } - - /** - * Lấy tất cả IPs trong range start → end (inclusive). - */ - private function ipsFromRange(string $start, string $end): array - { - $startInt = ip2long($start); - $endInt = ip2long($end); - - if ($startInt === false || $endInt === false || $startInt > $endInt) { - return []; - } - - $ips = []; - for ($i = $startInt; $i <= $endInt; $i++) { - $ips[] = long2ip($i); - } - - return $ips; - } -} diff --git a/database/seeders/VcenterSeeder.php b/database/seeders/VcenterSeeder.php deleted file mode 100644 index 4302a9a..0000000 --- a/database/seeders/VcenterSeeder.php +++ /dev/null @@ -1,23 +0,0 @@ - '10.10.10.6', - // 'user' => 'administrator@vsphere.local', - // 'password' => bcrypt('Tgs@1238989$%^'), - // 'status' => 1 - // ]); - } -} diff --git a/public/storage b/public/storage new file mode 120000 index 0000000..102dd1f --- /dev/null +++ b/public/storage @@ -0,0 +1 @@ +/Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal/storage/app/public \ No newline at end of file diff --git a/routes/client.php b/routes/client.php index d1acf93..809bd64 100644 --- a/routes/client.php +++ b/routes/client.php @@ -42,6 +42,7 @@ Route::prefix('v1') Route::get('me', 'me')->name('me'); Route::post('logout', 'logout')->name('logout'); Route::put('update', 'update')->name('update'); + Route::post('update-cccd', 'updateCccd')->name('update-cccd'); Route::put('change-password', 'changePassword')->name('change-password'); }); }); diff --git a/storage/app/public/cccd/9TDwwNooyCkrHLhiKZGDGTWXPxmr2EC9fyOMX1gM.jpg b/storage/app/public/cccd/9TDwwNooyCkrHLhiKZGDGTWXPxmr2EC9fyOMX1gM.jpg new file mode 100644 index 0000000..2426770 Binary files /dev/null and b/storage/app/public/cccd/9TDwwNooyCkrHLhiKZGDGTWXPxmr2EC9fyOMX1gM.jpg differ diff --git a/storage/app/public/cccd/c9LZL9tYpF7p4Ts4Ty6xVIPEo0ZIaQ9NJ8SSL7fK.png b/storage/app/public/cccd/c9LZL9tYpF7p4Ts4Ty6xVIPEo0ZIaQ9NJ8SSL7fK.png new file mode 100644 index 0000000..bf117e1 Binary files /dev/null and b/storage/app/public/cccd/c9LZL9tYpF7p4Ts4Ty6xVIPEo0ZIaQ9NJ8SSL7fK.png differ diff --git a/storage/app/public/tickets/cvxU7DwWWDbtEqjC76Ct943omX3fHuNWqL0qTmUo.png b/storage/app/public/tickets/cvxU7DwWWDbtEqjC76Ct943omX3fHuNWqL0qTmUo.png new file mode 100644 index 0000000..2fb2bc1 Binary files /dev/null and b/storage/app/public/tickets/cvxU7DwWWDbtEqjC76Ct943omX3fHuNWqL0qTmUo.png differ diff --git a/storage/app/public/tickets/o0VlcUWxsxdFvBYjJgtA6S09s6kZd2oVo8EB0VDy.jpg b/storage/app/public/tickets/o0VlcUWxsxdFvBYjJgtA6S09s6kZd2oVo8EB0VDy.jpg new file mode 100644 index 0000000..2426770 Binary files /dev/null and b/storage/app/public/tickets/o0VlcUWxsxdFvBYjJgtA6S09s6kZd2oVo8EB0VDy.jpg differ