update change password

This commit is contained in:
pbaodev
2025-08-24 22:41:15 +07:00
parent 6b358afd4b
commit 197fe413e5
19 changed files with 699 additions and 193 deletions
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\frontend;
use App\Http\Controllers\Controller;
use App\Http\Requests\Vm\SnapShotRequest;
use App\Jobs\VirtualMachine\Action\ResetVmPasswordJob;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Repositories\Invoice\InvoiceRepositoryInterface;
@@ -552,109 +553,50 @@ class ServiceController extends Controller
/**
* Reset VM password (Windows/Linux) via vCenter helpers
*/
public function resetVMPassword(Request $request, vCenter $serviceVCenter)
public function resetVMPassword(Request $request)
{
try {
$user = customer();
if (!$user) {
return response()->json([
'error' => true,
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.'),
], 401);
}
$validated = FacadesValidator::make($request->all(), [
'product_id' => 'required|string|max:255',
'new_password' => 'required|string|min:8|max:128',
]);
if ($validated->fails()) {
return response()->json([
'error' => true,
'message' => __('Dữ liệu không hợp lệ'),
'errors' => $validated->errors(),
], 422);
}
$productId = $this->sanitizeInput($request->product_id);
$newPassword = (string) $request->new_password;
// Load product and ensure ownership
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')
->where('vmware_id', $productId)
->where('customer_id', $user->id)
->first();
if (!$product) {
return response()->json([
'error' => true,
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập'),
], 404);
}
$vcenter = $product->ipAddress->host->datacenter->vCenter ?? null;
if (!$vcenter) {
return response()->json([
'error' => true,
'message' => __('Cấu hình vCenter không hợp lệ'),
], 400);
}
// Login to vCenter
if (!$serviceVCenter->loginServer($vcenter)) {
return response()->json([
'error' => true,
'message' => __('Không thể kết nối đến vCenter'),
], 500);
}
// Determine OS family
$osFamily = strtoupper($product->os->osGroup->name ?? '');
$isWindows = str_contains($osFamily, 'WINDOWS');
// Prepare data
$ip = $product->ipAddress->ip ?? null;
if (!$ip) {
return response()->json([
'error' => true,
'message' => __('Không tìm thấy IP của máy ảo'),
], 400);
}
$payload = [
'ip' => $ip,
'user' => $isWindows ? 'administrator' : 'root',
'password' => $product->password ?? 'TGS@123', // fallback if field exists; adjust per model
'new_password' => $newPassword,
];
$ok = $isWindows
? $serviceVCenter->change_password_windows($payload)
: $serviceVCenter->change_password_linux($payload);
if (!$ok) {
return response()->json([
'error' => true,
'message' => __('Không thể đặt lại mật khẩu. Vui lòng thử lại sau.'),
], 500);
}
return response()->json([
'error' => false,
'message' => __('Đặt lại mật khẩu thành công'),
]);
} catch (\Exception $e) {
Log::error('Lỗi reset VM password', [
'product_id' => $request->product_id ?? null,
'error' => $e->getMessage(),
]);
return response()->json([
'error' => true,
'message' => __('Có lỗi xảy ra khi đặt lại mật khẩu'),
], 500);
$user = customer();
if (!$user) {
return response()->json(['error' => true, 'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')], 401);
}
$validated = Validator::make($request->all(), [
'product_id' => 'required|string|max:255',
'new_password' => 'required|string|min:8|max:128',
'old_password' => 'required|string|min:8|max:128',
]);
if ($validated->fails()) {
return response()->json(['error' => true, 'message' => __('Dữ liệu không hợp lệ'), 'errors' => $validated->errors()], 422);
}
$productId = $this->sanitizeInput($request->product_id);
$newPassword = (string) $request->new_password;
$oldPassword = (string) $request->old_password;
// load & ownership check nhanh (không cần toàn bộ nặng nề vì Job sẽ retry)
$product = ModuleVmware::where('vmware_id', $productId)
->where('customer_id', $user->id)
->first();
if (!$product) {
return response()->json(['error' => true, 'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')], 404);
}
// Dispatch job với delay 5s
ResetVmPasswordJob::dispatch(
productId: $productId,
newPassword: $newPassword,
oldPassword: $oldPassword,
userId: $user->id,
email: $user->email
)->delay(now()->addSeconds(5));
// trả accepted để UI biết đang xử lý nền
return redirect()->back()->with('success', 'Đã gửi yêu cầu tạo đặt lại mật khẩu. Quá trình sẽ mất 3-5 phút, vui lòng chờ đợi.');
}
/**
* Change Remote Desktop port (Windows only)
*/
@@ -827,8 +769,8 @@ class ServiceController extends Controller
// Fetch product price based on invoice item link
$invoiceItem = InvoiceItem::whereHas('invoice', function ($q) use ($user) {
$q->where('customer_id', $user->id);
})
$q->where('customer_id', $user->id);
})
->where('product_type', 4) // VPS
->whereHas('invoice', fn($q) => $q->where('status', '!=', 0))
->first();
@@ -1131,7 +1073,7 @@ class ServiceController extends Controller
], 401);
}
$validator = \Validator::make($request->all(), [
$validator = Validator::make($request->all(), [
'product_id' => 'required|string|max:255',
]);
if ($validator->fails()) {
@@ -1158,7 +1100,7 @@ class ServiceController extends Controller
$result = $vCenterService->ShutdownGuestVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
// Clear VM info cache after stopping VM
\Cache::forget('vm_info_' . $product->vmware_id);
Cache::forget('the_gioi_so_database_the_gioi_so_cache:vm_info_' . $product->vmware_id);
return response()->json([
'error' => false,
'message' => __('Yêu cầu tắt máy ảo đã được gửi thành công.')
@@ -1287,9 +1229,9 @@ class ServiceController extends Controller
// Debug: Log invoice lookup
Log::info('Looking for invoice', ['invoice_id' => $request->invoice_id]);
$invoice = Invoice::find($request->invoice_id);
// Debug: Log invoice result
Log::info('Invoice lookup result', [
'invoice_id' => $request->invoice_id,
+157 -18
View File
@@ -15,7 +15,6 @@ use phpseclib3\Net\SSH2;
class vCenter
{
protected $process = null;
public ?string $token = null;
public string $apiUri = '10.10.10.6';
private string $username = 'administrator@vsphere.local';
@@ -23,7 +22,8 @@ class vCenter
private int $diskCapacity = 1073741824;
private int $memory = 1024;
protected string $os;
private ?string $os = null;
private SSH2|WinRMClient|null $process = null;
public function loginServer(\App\Models\Vcenter $vcenterModel): bool|string
{
@@ -325,6 +325,142 @@ class vCenter
}
}
// Login SSH
public function loginSSH(): bool
{
$conn = $this->LoginAuth();
if ($conn) {
Log::info("✅ Connected to vCenter via SSH successfully");
return true;
}
Log::error("❌ Failed to connect to vCenter via SSH");
return false;
}
public function runPsCommand(string $psCommand): string
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
// gọi trực tiếp -Command, thu cả STDERR
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass '
. '-Command "' . str_replace('"', '\"', $psCommand) . '"';
try {
$stream = ssh2_exec($auth, $action);
if (!$stream)
throw new \RuntimeException("ssh2_exec failed");
$err = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
stream_set_blocking($stream, true);
stream_set_blocking($err, true);
$out = stream_get_contents($stream) ?: '';
$errOut = stream_get_contents($err) ?: '';
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
protected function CommandRSP(string $cmd)
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
// Escape để dùng trong chuỗi single-quoted của PowerShell
$esc = fn(string $v) => str_replace("'", "''", $v);
$server = $esc($this->apiUri ?? '');
$user = $esc($this->username ?? '');
$pass = $esc($this->password ?? '');
// Ghép script PowerShell hoàn chỉnh, chạy trong 1 scriptblock
// Lưu ý: dùng placeholder rồi strtr để tránh lẫn dấu ngoặc
$psTemplate = <<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
# Thực thi lệnh công việc (đã được xây dựng sẵn, ví dụ: & 'C:\path\script.ps1' -Param 'Value' ... )
& {
<CMD>
} | Out-String
PS;
// Chèn dữ liệu
$psScript = strtr($psTemplate, [
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd, // ở đây $cmd KHÔNG cần ngoặc kép bao ngoài
]);
// Encode UTF-16LE → Base64 cho -EncodedCommand
$encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8'));
// Dùng -EncodedCommand để tránh mọi lỗi escape/quote
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$stream = ssh2_exec($auth, $action);
if (!$stream)
return "ssh2_exec failed";
$err = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
stream_set_blocking($stream, true);
stream_set_blocking($err, true);
$out = stream_get_contents($stream) ?: '';
$errOut = stream_get_contents($err) ?: '';
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
public function resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string
{
// File ps1 chạy được ngoài PowerShell: bạn đã test ok
$psFile = "C:\\CLISave\\AssignIP\\SetGuestPwd.ps1";
// Escape chuỗi cho PowerShell single-quoted
$q = fn(string $v) => str_replace("'", "''", $v);
// Xây args kiểu: -Key 'Value'
$args = " -VMName '" . $q($vmName) . "'";
$args .= " -GuestUser '" . $q($guestUser) . "'";
$args .= " -GuestPassword '" . $q($guestPassword) . "'";
$args .= " -TargetUser '" . $q($targetUser) . "'";
$args .= " -NewPassword '" . $q($newPassword) . "'";
// Gọi file trong cùng phiên PS đã Connect-VIServer (nhờ Command())
// Thêm Out-String để ép JSON nằm trên STDOUT gọn
$cmd = "& '$psFile'$args | Out-String";
$raw = $this->CommandRSP($cmd); // <-- dùng lại Command() của bạn
if ($raw === 'Login_Fail' || $raw === false || $raw === null) {
return "Login_Fail";
}
// Thử parse JSON (có thể lẫn banner/log)
$trim = trim($raw);
$data = json_decode($trim, true);
if (json_last_error() !== JSON_ERROR_NONE) {
if (preg_match('/(\{.*\}|\[.*\])/s', $trim, $m)) {
$data = json_decode($m[1], true);
}
}
return $data ?: $raw; // nếu chưa parse được, trả raw để debug
}
private function getAllNetwork()
{
$check = $this->checkApiExits();
@@ -457,7 +593,7 @@ class vCenter
try {
Log::info('Bắt đầu clone VM với timeout 300 giây', ['template' => $template, 'target_name' => $body['spec']['name']]);
$response = Http::withoutVerifying()
->timeout(300) // Tăng timeout lên 5 phút cho quá trình clone
->connectTimeout(30) // Giữ connect timeout ngắn để phát hiện lỗi kết nối sớm
@@ -1854,8 +1990,11 @@ class vCenter
return false;
}
}
public function change_password_linux(array $data = ['ip' => '103.42.58.37', 'user' => 'root', 'password' => 'TGS@123', 'new_password' => '']): bool
/*
* ip = 103.42.58.37
* pwd = TGS@123
*/
public function change_password_linux(array $data = ['ip' => '10.10.10.173', 'user' => 'admin', 'password' => 'TGS@123', 'new_password' => '']): bool
{
try {
// Kiểm tra mật khẩu mới
@@ -2139,14 +2278,14 @@ class vCenter
$dns = '8.8.8.8';
$vmware_name = '';
$vCenter = null;
// Check if it's ModuleVmware (new structure)
if ($product && isset($product->ipAddress) && $product->ipAddress) {
Log::info('✅ Using ModuleVmware structure for assignIPWindows', [
'module_vmware_id' => $product->id,
'vmware_id' => $vmware_id
]);
// Extract from ModuleVmware->ipAddress relationship
$ipAddress = $product->ipAddress->ip ?? null;
$gateway = $product->ipAddress->gateway ?? null;
@@ -2154,13 +2293,13 @@ class vCenter
$dns = $product->ipAddress->dns ?? '8.8.8.8';
$vmware_name = $product->name ?? $vmware_id;
$vCenter = $product->ipAddress->host->datacenter->vCenter ?? null;
} elseif ($product && isset($product->IpAddress) && $product->IpAddress) {
Log::info('✅ Using old Product structure for assignIPWindows', [
'product_id' => $product->id,
'vmware_id' => $vmware_id
]);
// Extract from old Product->IpAddress relationship
$ipAddress = $product->IpAddress->ip ?? null;
$gateway = $product->IpAddress->gateway ?? null;
@@ -2169,7 +2308,7 @@ class vCenter
$vmware_name = $product->name ?? $vmware_id;
$vCenter = $product->IpAddress->Host->Datacenter->vCenter ?? null;
}
// Validate required data
if (!$ipAddress || !$gateway || !$vCenter) {
Log::error('❌ assignIPWindows: Missing required data', [
@@ -2186,10 +2325,10 @@ class vCenter
}
$this->SetConfig($vCenter);
// Convert netmask to prefix length (e.g., 255.255.255.0 = 24)
$maskBits = $this->convertNetmaskToPrefixLength($netmask);
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Assign IP to Windows VM - $vmware_name");
Log::channel('Custom')->info("IP: $ipAddress, Gateway: $gateway, DNS: $dns, MaskBits: $maskBits");
@@ -2197,14 +2336,14 @@ class vCenter
// Create PowerShell command with dynamic values
$Command = "\$vmName = '$vmware_name'; \$IP = '$ipAddress'; \$MaskBits = $maskBits; \$Gateway = '$gateway'; \$DNS = '$dns'; cd /; C:\CLISave\AssignIP\assignIPWindows.ps1;";
Log::channel('Custom')->info("Executing Command: $Command");
$output = $this->Command($Command);
Log::channel('Custom')->info("Script output: " . $output);
Log::channel('Custom')->info("===================================================================");
// Check if execution was successful
if (strpos($output, '🎉') !== false || strpos($output, 'successfully') !== false) {
return [
@@ -2219,7 +2358,7 @@ class vCenter
'output' => $output
];
}
} catch (\Exception $e) {
Log::channel('Custom')->error("Exception in assignIPWindows: " . $e->getMessage());
return [
@@ -2261,7 +2400,7 @@ class vCenter
'255.128.0.0' => 9,
'255.0.0.0' => 8
];
return $netmaskMap[$netmask] ?? 24; // Default to /24 if not found
}
@@ -0,0 +1,101 @@
<?php
namespace App\Jobs\VirtualMachine\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Http\Services\vCenter;
use App\Mail\VmPasswordResetSuccess;
use App\Models\ModuleVmware;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class ResetVmPasswordJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 180;
public function __construct(
public string $productId,
public string $oldPassword,
public string $newPassword,
public int $userId,
public string $email
) {}
public function handle(vCenter $serviceVCenter): void
{
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter','os.osGroup')
->where('vmware_id', $this->productId)
->where('customer_id', $this->userId)
->firstOrFail();
$osFamily = (string)($product->os->osGroup->name ?? '');
$isWindows = stripos($osFamily, 'windows') !== false;
if (!$isWindows) {
throw new \RuntimeException('Only Windows is supported by this reset job.');
}
$vmName = $product->ipAddress->vm_name
?? $product->name
?? $product->hostname
?? null;
if (!$vmName && preg_match('/^vm-\d+$/i', (string)$product->vmware_id)) {
$rawName = trim($serviceVCenter->runPsCommand("(Get-VM -Id '".addslashes($product->vmware_id)."').Name"));
if (($p = strpos($rawName, '[STDERR]')) !== false) {
$rawName = trim(substr($rawName, 0, $p));
}
$vmName = trim(preg_replace('/\s+/', ' ', $rawName));
}
if (!$vmName) {
throw new \RuntimeException('Cannot resolve VM Name on vCenter.');
}
$guestUser = 'Administrator';
$guestPassword = $this->oldPassword;
$result = $serviceVCenter->resetGuestPasswordViaPs1(
vmName: $vmName,
guestUser: $guestUser,
guestPassword: $guestPassword,
newPassword: $this->newPassword,
);
$product->update(['password' => encrypt($this->newPassword)]);
if ($result === 'Login_Fail' || $result === false || $result === null) {
throw new \RuntimeException('SSH runner login failed.');
}
if (is_string($result)) {
throw new \RuntimeException('Script returned non-JSON output: '.$result);
}
$row = $result;
if (isset($result[0])) {
$row = $result[0];
foreach ($result as $it) {
if (is_array($it) && (($it['vm'] ?? null) === $vmName)) { $row = $it; break; }
}
}
$status = strtolower($row['status'] ?? '');
if ($status !== 'ok') {
throw new \RuntimeException($row['message'] ?? 'Reset failed by script');
}
// Gửi mail thông báo
Mail::to($this->email)->send(
new VmPasswordResetSuccess(vmName: $vmName, newPassword: $this->newPassword)
);
Log::info('ResetVmPasswordJob: success', ['vm'=>$vmName]);
}
}
@@ -104,16 +104,17 @@ class CreateVirtualMachineJob implements ShouldQueue
'ram' => 0,
'disk' => 0
];
// Lấy addon từ order item
if ($this->item->addons) {
foreach ($this->item->addons as $orderAddon) {
$addon = $orderAddon->addonItems;
if (!$addon) continue;
if (!$addon)
continue;
$addonName = strtolower(trim($addon->name));
$quantity = (int) $orderAddon->qty;
switch ($addonName) {
case 'cpu':
$addonData['cpu'] += $quantity;
@@ -187,10 +188,10 @@ class CreateVirtualMachineJob implements ShouldQueue
return false;
} else {
Log::info('Tạo máy ảo thành công: ' . json_encode($result));
// Lấy vm_id từ kết quả
$vmId = is_array($result) ? ($result['data'] ?? 'unknown') : $result;
if ($vmId !== 'unknown' && $vmId !== 'UNKNOWN') {
try {
// Lưu vm_id vào bảng invoice
@@ -212,11 +213,11 @@ class CreateVirtualMachineJob implements ShouldQueue
try {
$customer = $this->item->order->customer;
$serverName = $this->item->server_name ?? 'VM-' . $vmId;
// 1. Lấy IP available từ IPPool
$networkId = $product->ipAddress?->module_network_id ?? 1;
$availableIP = $this->getAvailableIPFromPool($networkId);
// 2. Tạo ModuleIpAddress với IP từ pool
$moduleIpAddress = ModuleIpAddress::create([
'vm_name' => $serverName,
@@ -228,7 +229,7 @@ class CreateVirtualMachineJob implements ShouldQueue
'dns' => $product->ipAddress?->dns ?? '8.8.8.8',
'status' => 1 // 1 = Assigned
]);
// 3. Tạo ModuleVmware với IP address vừa tạo
$moduleVmware = ModuleVmware::create([
'name' => $serverName,
@@ -239,7 +240,7 @@ class CreateVirtualMachineJob implements ShouldQueue
'next_payment' => $this->item->order->next_payment ?? 0,
'next_due_date' => $this->item->order->next_due_date ?? null,
'next_invoice' => $this->item->order->next_invoice ?? 0,
'user' => 'root',
'user' => 'Administrator',
'password' => '',
'hostname' => $serverName,
'vmware_id' => $vmId,
@@ -247,7 +248,7 @@ class CreateVirtualMachineJob implements ShouldQueue
'customer_id' => $customer->id,
'status' => 1
]);
Log::info('✅ Lưu dữ liệu VM và IP address thành công', [
'vmware_id' => $vmId,
'module_vmware_id' => $moduleVmware->id,
@@ -256,18 +257,18 @@ class CreateVirtualMachineJob implements ShouldQueue
'name' => $serverName,
'order_item_id' => $this->item->id
]);
// Dispatch AssignIPJob để chạy sau 5 phút
AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip)
->delay(now()->addMinutes(5));
Log::info('✅ Đã dispatch AssignIPJob để chạy sau 5 phút', [
'vm_id' => $vmId,
'module_vmware_id' => $moduleVmware->id,
'assigned_ip' => $moduleIpAddress->ip,
'delay_minutes' => 5
]);
} catch (\Exception $e) {
Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [
'vmware_id' => $vmId,
@@ -284,7 +285,7 @@ class CreateVirtualMachineJob implements ShouldQueue
]);
}
}
// Gửi thông báo thành công
$telegram->SendJobRunInfo(
'CreateVirtualMachineJob',
@@ -333,22 +334,22 @@ class CreateVirtualMachineJob implements ShouldQueue
$ipPoolList = IpPool::where('module_network_id', $networkId)
->orderBy('ip_address')
->get();
Log::info('✅ Lấy IP available từ pool', [
'ip_pool_list' => $ipPoolList,
'network_id' => $networkId
]);
if ($ipPoolList->isEmpty()) {
// Nếu không có IP trong pool, trả về IP default
return '0.0.0.0';
}
// Lấy tất cả IP đã được assign trong ModuleIpAddress
$assignedIPs = ModuleIpAddress::where('module_network_id', $networkId)
->pluck('ip')
->toArray();
// Tìm IP đầu tiên chưa được assign
foreach ($ipPoolList as $ipPool) {
if (!in_array($ipPool->ip_address, $assignedIPs)) {
@@ -359,14 +360,14 @@ class CreateVirtualMachineJob implements ShouldQueue
return $ipPool->ip_address;
}
}
// Nếu tất cả IP đã được assign, log warning và trả về IP default
Log::warning('⚠️ Không có IP available trong pool', [
'network_id' => $networkId,
'total_ips_in_pool' => $ipPoolList->count(),
'assigned_ips' => count($assignedIPs)
]);
return '0.0.0.0';
}
@@ -381,21 +382,21 @@ class CreateVirtualMachineJob implements ShouldQueue
if (!$check) {
return 'Login_Again';
}
// Gọi API start VM trực tiếp
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $vCenter->token
])
->post("https://{$vCenter->apiUri}/rest/vcenter/vm/{$vmId}/power/start");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response->body()
@@ -29,10 +29,10 @@ class SyncAllPackage implements ShouldQueue
*/
public function handle()
{
SyncDatacenter::dispatchSync();
SyncHost::dispatchSync();
SyncDatastore::dispatchSync();
SyncNetwork::dispatchSync();
SyncFolder::dispatchSync();
SyncDatacenter::dispatch();
SyncHost::dispatch();
SyncDatastore::dispatch();
SyncNetwork::dispatch();
SyncFolder::dispatch();
}
}
+1 -1
View File
@@ -86,7 +86,7 @@ class SyncNetwork implements ShouldQueue
'name' => $_subItem['name'],
'network' => $_subItem['network'],
'type' => $_subItem['type'],
'ModuleHost_id' => $hostItem->id
'module_host_id' => $hostItem->id
]);
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class VmPasswordResetSuccess extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public string $vmName,
public string $newPassword
) {}
public function build()
{
return $this->subject('Mật khẩu VPS của bạn đã được đặt lại')
->view('emails.vm_password_reset_success');
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
parent::boot();
// Horizon::routeSmsNotificationsTo('15556667777');
// Horizon::routeMailNotificationsTo('example@example.com');
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
}
/**
* Register the Horizon gate.
*
* This gate determines who can access Horizon in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewHorizon', function ($user = null) {
return in_array(optional($user)->email, [
//
]);
});
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Services\Cache;
use App\Services\Caches\CacheServiceInterface;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class CacheService implements CacheServiceInterface
{
protected int $ttl;
public function __construct(int $ttl = 60)
{
$this->ttl = $ttl;
}
public function putCache(string $prefix, string $key, mixed $value, ?int $ttl = null): void
{
$cacheKey = $this->makeKey($prefix, $key);
Cache::put($cacheKey, $value, $ttl ?? $this->ttl);
}
public function getCache(string $prefix, string $key, mixed $default = null): mixed
{
$cacheKey = $this->makeKey($prefix, $key);
return Cache::get($cacheKey, $default);
}
public function clearCache(string $prefix, string $key): void
{
$cacheKey = $this->makeKey($prefix, $key);
Cache::forget($cacheKey);
}
public function clearCacheByPrefix(string $prefix): void
{
$cachePrefix = config('cache.prefix');
$pattern = $cachePrefix . $prefix . '_*';
$keys = Redis::connection('cache')->keys($pattern);
foreach ($keys as $k) {
Redis::connection('cache')->del($k);
}
}
protected function makeKey(string $prefix, string $key = ''): string
{
return trim($prefix . '_' . $key, '_');
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Services\Caches;
interface CacheServiceInterface
{
public function putCache(string $prefix, string $key, mixed $value, ?int $ttl = null): void;
public function getCache(string $prefix, string $key, mixed $default = null): mixed;
public function clearCache(string $prefix, string $key): void;
public function clearCacheByPrefix(string $prefix): void;
}
+1
View File
@@ -181,6 +181,7 @@ return [
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\RepositoryProvider::class
+213
View File
@@ -0,0 +1,213 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Horizon Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Horizon will be accessible from. If this
| setting is null, Horizon will reside under the same domain as the
| application. Otherwise, this value will serve as the subdomain.
|
*/
'domain' => env('HORIZON_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Horizon Path
|--------------------------------------------------------------------------
|
| This is the URI path where Horizon will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => env('HORIZON_PATH', 'horizon'),
/*
|--------------------------------------------------------------------------
| Horizon Redis Connection
|--------------------------------------------------------------------------
|
| This is the name of the Redis connection where Horizon will store the
| meta information required for it to function. It includes the list
| of supervisors, failed jobs, job metrics, and other information.
|
*/
'use' => 'default',
/*
|--------------------------------------------------------------------------
| Horizon Redis Prefix
|--------------------------------------------------------------------------
|
| This prefix will be used when storing all Horizon data in Redis. You
| may modify the prefix when you are running multiple installations
| of Horizon on the same server so that they don't have problems.
|
*/
'prefix' => env(
'HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
),
/*
|--------------------------------------------------------------------------
| Horizon Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will get attached onto each Horizon route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Queue Wait Time Thresholds
|--------------------------------------------------------------------------
|
| This option allows you to configure when the LongWaitDetected event
| will be fired. Every connection / queue combination may have its
| own, unique threshold (in seconds) before this event is fired.
|
*/
'waits' => [
'redis:default' => 60,
],
/*
|--------------------------------------------------------------------------
| Job Trimming Times
|--------------------------------------------------------------------------
|
| Here you can configure for how long (in minutes) you desire Horizon to
| persist the recent and failed jobs. Typically, recent jobs are kept
| for one hour while all failed jobs are stored for an entire week.
|
*/
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080,
'failed' => 10080,
'monitored' => 10080,
],
/*
|--------------------------------------------------------------------------
| Silenced Jobs
|--------------------------------------------------------------------------
|
| Silencing a job will instruct Horizon to not place the job in the list
| of completed jobs within the Horizon dashboard. This setting may be
| used to fully remove any noisy jobs from the completed jobs list.
|
*/
'silenced' => [
// App\Jobs\ExampleJob::class,
],
/*
|--------------------------------------------------------------------------
| Metrics
|--------------------------------------------------------------------------
|
| Here you can configure how many snapshots should be kept to display in
| the metrics graph. This will get used in combination with Horizon's
| `horizon:snapshot` schedule to define how long to retain metrics.
|
*/
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
/*
|--------------------------------------------------------------------------
| Fast Termination
|--------------------------------------------------------------------------
|
| When this option is enabled, Horizon's "terminate" command will not
| wait on all of the workers to terminate unless the --wait option
| is provided. Fast termination can shorten deployment delay by
| allowing a new instance of Horizon to start while the last
| instance will continue to terminate each of its workers.
|
*/
'fast_termination' => false,
/*
|--------------------------------------------------------------------------
| Memory Limit (MB)
|--------------------------------------------------------------------------
|
| This value describes the maximum amount of memory the Horizon master
| supervisor may consume before it is terminated and restarted. For
| configuring these limits on your workers, see the next section.
|
*/
'memory_limit' => 64,
/*
|--------------------------------------------------------------------------
| Queue Worker Configuration
|--------------------------------------------------------------------------
|
| Here you may define the queue worker settings used by your application
| in all environments. These supervisors and settings handle all your
| queued jobs and will be provisioned by Horizon during deployment.
|
*/
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'maxProcesses' => 1,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
'timeout' => 60,
'nice' => 0,
],
],
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
'local' => [
'supervisor-1' => [
'maxProcesses' => 3,
],
],
],
];
@@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('invoice', function (Blueprint $table) {
$table->unsignedInteger('rebuild_count')->nullable()->default(0)->after('vm_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('invoice', function (Blueprint $table) {
//
});
}
};
+1
View File
@@ -29,5 +29,6 @@ class DatabaseSeeder extends Seeder
$this->call(SnapshotLimitedSeeder::class);
$this->call(TemplateSeeder::class);
$this->call(Setting::class);
$this->call(MenuSeeder::class);
}
}
-2
View File
@@ -174,5 +174,3 @@
<button type="submit" class="btn btn-outline-primary">Lưu dữ liệu</button>
</div>
</form>
@@ -0,0 +1,5 @@
<p>Xin chào,</p>
<p>Mật khẩu VPS <strong>{{ $vmName }}</strong> đã được đặt lại thành công.</p>
<p>Mật khẩu mới: <code>{{ $newPassword }}</code></p>
<p>Vui lòng đăng nhập và đổi lại mật khẩu theo ý bạn.</p>
<p>Trân trọng,</p>
@@ -41,7 +41,7 @@
<span class="text-gray-600 font-medium">Password</span>
<div class="flex items-center gap-2">
<span class="font-mono text-sm bg-gray-100 px-2 py-1 rounded"
x-text="passwordVisible ? 'TGS@123' : '••••••••'">
x-text="passwordVisible ? '' : '••••••••'">
</span>
<button type="button" @click="passwordVisible = !passwordVisible"
class="text-blue-600 hover:text-blue-800" title="{{ __('Toggle password visibility') }}">
@@ -9,24 +9,31 @@
</flux:text>
</div>
<form x-data="{ submitting: false }" x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST" action="{{ route('service.reset.vm.password') }}">
<form x-data="{ submitting: false }" x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()"
method="POST" action="{{ route('service.reset.vm.password') }}">
@csrf
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
<flux:field>
<flux:label>{{ __('Mật khẩu cũ') }}</flux:label>
<flux:input name="old_password" type="password" placeholder="{{ __('Nhập mật khẩu cũ') }}" required
minlength="8" />
</flux:field>
<flux:field>
<flux:label>{{ __('Mật khẩu mới') }}</flux:label>
<flux:input name="new_password" type="password" placeholder="{{ __('Nhập mật khẩu mới') }}" required minlength="8" />
<flux:description>{{ $isWindows ? __('Tài khoản: administrator') : __('Tài khoản: root') }}</flux:description>
<flux:input name="new_password" type="password" placeholder="{{ __('Nhập mật khẩu mới') }}" required
minlength="8" />
<flux:description>{{ $isWindows ? __('Tài khoản: administrator') : __('Tài khoản: root') }}
</flux:description>
</flux:field>
<div class="flex gap-2 justify-end mt-6">
<flux:modal.close>
<flux:button variant="ghost">{{ __('Hủy') }}</flux:button>
</flux:modal.close>
<flux:button type="submit" variant="primary" color="orange" x-bind:disabled="submitting">{{ __('Đồng ý') }}</flux:button>
<flux:button type="submit" variant="primary" color="orange" x-bind:disabled="submitting">
{{ __('Đồng ý') }}</flux:button>
</div>
</form>
</div>
</flux:modal>
@@ -45,8 +45,7 @@
@include('frontend.service.partials.control-buttons.snapshot-button')
@include('frontend.service.partials.control-buttons.reset-password-button')
@include('frontend.service.partials.control-buttons.change-ip-button')
@endif
@endif
</div>
{{-- Notes Section --}}
@@ -62,7 +61,7 @@
'isWindows' => $isWindows,
])
@include('frontend.service.partials.change-ip-modal', ['invoice' => $invoice])
@include('frontend.service.partials.change-remote-port-modal', ['invoice' => $invoice])
@include('frontend.service.partials.change-remote-port-modal', ['invoice' => $invoice])
@include('frontend.service.partials.renewal-modal', ['invoice' => $invoice])
@endif
</div>