From 5b0b6de2dc2a9b6aab8895f627175e6b642d1a6c Mon Sep 17 00:00:00 2001 From: dyzulk <66510723+dyzulk@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:01:05 +0700 Subject: [PATCH] Chore: Bump version to v1.1.0 and implement automated release system --- .dockerignore | 1 + .gitattributes | 11 +- .github/workflows/release.yml | 53 +++ .gitignore | 7 +- app/Config/SiteConfig.php | 2 +- app/Controllers/ApiController.php | 3 +- app/Controllers/DashboardController.php | 7 +- app/Controllers/DhcpController.php | 5 +- app/Controllers/GeneratorController.php | 5 +- app/Controllers/HotspotController.php | 31 +- app/Controllers/LogController.php | 5 +- app/Controllers/ProfileController.php | 26 +- app/Controllers/PublicStatusController.php | 10 +- app/Controllers/QuickPrintController.php | 61 ++- app/Controllers/SettingsController.php | 52 +-- ...ller.php => VoucherTemplateController.php} | 17 +- app/Core/Console.php | 2 +- app/Core/Migrations.php | 3 + app/Core/Router.php | 129 +++++- app/Helpers/ErrorHelper.php | 15 +- app/Middleware/AuthMiddleware.php | 15 + app/Middleware/CorsMiddleware.php | 39 ++ app/Middleware/MiddlewareInterface.php | 7 + app/Middleware/RouterCheckMiddleware.php | 41 ++ app/Models/Config.php | 1 + app/Models/Logo.php | 8 +- app/Models/QuickPrintModel.php | 21 +- app/Models/VoucherTemplateModel.php | 8 +- app/Views/dashboard.php | 3 +- app/Views/errors/default.php | 12 +- app/Views/hotspot/profiles/add.php | 236 ----------- app/Views/hotspot/profiles/edit.php | 241 ----------- app/Views/hotspot/profiles/index.php | 229 +++++++++- app/Views/hotspot/users/add.php | 171 -------- app/Views/hotspot/users/edit.php | 134 ------ app/Views/hotspot/users/users.php | 398 +++++++++++++----- app/Views/layouts/footer_main.php | 124 +++++- app/Views/layouts/footer_public.php | 51 +-- app/Views/layouts/header_main.php | 14 +- app/Views/layouts/header_public.php | 135 +++++- app/Views/layouts/navbar_main.php | 34 +- app/Views/layouts/sidebar_session.php | 46 +- app/Views/layouts/sidebar_settings.php | 2 +- app/Views/public/status.php | 67 ++- app/Views/quick_print/list.php | 246 ++++++----- app/Views/settings/api_cors.php | 200 +++------ app/Views/settings/form.php | 123 ------ app/Views/settings/index.php | 260 +++++++++++- app/Views/settings/logos.php | 4 +- .../{templates => voucher_templates}/add.php | 0 .../{templates => voucher_templates}/edit.php | 4 +- .../index.php | 10 +- app/Views/system/scheduler.php | 219 ++++------ package.json | 4 +- public/assets/css/styles.css | 274 +++++++++--- .../assets/js/{ => components}/datatable.js | 218 +++++----- public/assets/js/components/select.js | 252 +++++++++++ public/assets/js/custom-select.js | 261 ------------ public/assets/js/mivo.js | 70 +++ .../js/{alert-helper.js => modules/alert.js} | 132 ++++-- public/assets/js/{ => modules}/i18n.js | 47 ++- public/assets/js/modules/updater.js | 112 +++++ public/assets/js/router-form.js | 98 ----- public/lang/en.json | 73 +++- public/lang/id.json | 73 +++- .../{assets/img/logos => uploads}/.gitignore | 0 routes/api.php | 19 +- routes/web.php | 254 ++++++----- src/input.css | 97 ++++- 69 files changed, 3157 insertions(+), 2375 deletions(-) create mode 100644 .github/workflows/release.yml rename app/Controllers/{TemplateController.php => VoucherTemplateController.php} (86%) create mode 100644 app/Middleware/AuthMiddleware.php create mode 100644 app/Middleware/CorsMiddleware.php create mode 100644 app/Middleware/MiddlewareInterface.php create mode 100644 app/Middleware/RouterCheckMiddleware.php delete mode 100644 app/Views/hotspot/profiles/add.php delete mode 100644 app/Views/hotspot/profiles/edit.php delete mode 100644 app/Views/hotspot/users/add.php delete mode 100644 app/Views/hotspot/users/edit.php delete mode 100644 app/Views/settings/form.php rename app/Views/settings/{templates => voucher_templates}/add.php (100%) rename app/Views/settings/{templates => voucher_templates}/edit.php (98%) rename app/Views/settings/{templates => voucher_templates}/index.php (94%) rename public/assets/js/{ => components}/datatable.js (58%) create mode 100644 public/assets/js/components/select.js delete mode 100644 public/assets/js/custom-select.js create mode 100644 public/assets/js/mivo.js rename public/assets/js/{alert-helper.js => modules/alert.js} (52%) rename public/assets/js/{ => modules}/i18n.js (67%) create mode 100644 public/assets/js/modules/updater.js delete mode 100644 public/assets/js/router-form.js rename public/{assets/img/logos => uploads}/.gitignore (100%) diff --git a/.dockerignore b/.dockerignore index 6336673..2e68746 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,4 @@ docs/ app/Database/*.sqlite public/assets/img/logos/* !public/assets/img/logos/.gitignore +CNAME diff --git a/.gitattributes b/.gitattributes index 9ed8796..7924e0f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,11 +2,20 @@ /docs export-ignore /.github export-ignore /docker export-ignore +/CNAME export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.dockerignore export-ignore /.env.example export-ignore -/deploy_package.tar.gz export-ignore +/package.json export-ignore +/package-lock.json export-ignore +/tailwind.config.js export-ignore +/src export-ignore +/Dockerfile export-ignore +/docker-compose.yml export-ignore /DOCKER_README.md export-ignore +/build_release.ps1 export-ignore +/deploy.ps1 export-ignore +/serve.bat export-ignore /phpstan.neon export-ignore /phpunit.xml export-ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6af229d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Create Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: mbstring, xml, ctype, iconv, sqlite3, openssl + coverage: none + + - name: Get version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create Release Directory + run: | + mkdir release_temp + # Export source using git archive (respects .gitattributes) + git archive --format=tar HEAD | tar -x -C release_temp + + - name: Install Production Dependencies + run: | + cd release_temp + composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs + + - name: Build Zip Artifact + run: | + cd release_temp + zip -r ../mivo-v${{ steps.get_version.outputs.VERSION }}.zip . + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: mivo-v${{ steps.get_version.outputs.VERSION }}.zip + generate_release_notes: true + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 200f3d1..e1e3016 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ Thumbs.db # Build Artifacts & Deployments /deploy_package.tar.gz /mivo_backup_*.mivo +/mivo-*.zip # Secrets and Environment .env @@ -27,4 +28,8 @@ docs/.vitepress/cache # Build Scripts & Artifacts build_release.ps1 -*.zip \ No newline at end of file +deploy.ps1 + +# User Uploads +/public/uploads/* +!/public/uploads/.gitignore diff --git a/app/Config/SiteConfig.php b/app/Config/SiteConfig.php index c5d50f1..a974164 100644 --- a/app/Config/SiteConfig.php +++ b/app/Config/SiteConfig.php @@ -3,7 +3,7 @@ namespace App\Config; class SiteConfig { const APP_NAME = 'MIVO'; - const APP_VERSION = 'v1.0'; + const APP_VERSION = 'v1.1.0'; const APP_FULL_NAME = 'MIVO - Mikrotik Voucher'; const CREDIT_NAME = 'DyzulkDev'; const CREDIT_URL = 'https://dyzulk.com'; diff --git a/app/Controllers/ApiController.php b/app/Controllers/ApiController.php index b9592b5..beb4939 100644 --- a/app/Controllers/ApiController.php +++ b/app/Controllers/ApiController.php @@ -32,7 +32,8 @@ class ApiController extends Controller { $configModel = new Config(); $session = $configModel->getSessionById($id); if ($session && !empty($session['password'])) { - $pass = EncryptionHelper::decrypt($session['password']); + // Config::getSessionById already decrypts the password + $pass = $session['password']; } } diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index e58bfaa..25d6a55 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -10,7 +10,7 @@ use App\Core\Middleware; class DashboardController extends Controller { public function __construct() { - Middleware::auth(); + // Auth handled by Router Middleware } public function index($session) { @@ -101,6 +101,7 @@ class DashboardController extends Controller { 'hotspot_users' => 'Hotspot Users', 'hotspot_users' => 'Hotspot Users', ], + 'reload_interval' => $creds['reload'] ?? 5, // Default 5s if not set 'interface' => $creds['interface'] ?? 'ether1' ]; // Pass Users Link (Optional: could be part of layout or card link) @@ -108,7 +109,9 @@ class DashboardController extends Controller { return $this->view('dashboard', $data); } else { - echo "Connection Failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/')); + exit; } } } diff --git a/app/Controllers/DhcpController.php b/app/Controllers/DhcpController.php index ae22a5f..90c40df 100644 --- a/app/Controllers/DhcpController.php +++ b/app/Controllers/DhcpController.php @@ -26,7 +26,10 @@ class DhcpController extends Controller if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { // Fetch DHCP Leases $leases = $API->comm("/ip/dhcp-server/lease/print"); - $API->disconnect(); + } else { + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $config['ip_address']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } // Add index for viewing diff --git a/app/Controllers/GeneratorController.php b/app/Controllers/GeneratorController.php index 1d31b90..b1b7df5 100644 --- a/app/Controllers/GeneratorController.php +++ b/app/Controllers/GeneratorController.php @@ -34,8 +34,9 @@ class GeneratorController extends Controller { $this->view('hotspot/generate', $data); } else { - // Handle connection error (flash message ideally, but for now redirect or show error) - echo "Connection failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } } diff --git a/app/Controllers/HotspotController.php b/app/Controllers/HotspotController.php index 3fc9054..8adaff3 100644 --- a/app/Controllers/HotspotController.php +++ b/app/Controllers/HotspotController.php @@ -25,6 +25,7 @@ class HotspotController extends Controller { $userId = $session; // For view context $users = []; + $servers = []; $error = null; $API = new RouterOSAPI(); @@ -40,17 +41,20 @@ class HotspotController extends Controller { // Get all hotspot users $users = $API->comm("/ip/hotspot/user/print"); - // Get active users to mark status (optional, can be done later for optimization) - // $active = $API->comm("/ip/hotspot/active/print"); + // Get servers for dropdown + $servers = $API->comm("/ip/hotspot/server/print"); $API->disconnect(); } else { - $error = "Connection Failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } $data = [ 'session' => $session, 'users' => $users, + 'servers' => $servers, 'error' => $error ]; @@ -389,7 +393,9 @@ class HotspotController extends Controller { $items = $API->comm("/ip/hotspot/active/print"); $API->disconnect(); } else { - $error = "Connection Failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } $data = [ @@ -451,7 +457,9 @@ class HotspotController extends Controller { $items = $API->comm("/ip/hotspot/host/print"); $API->disconnect(); } else { - $error = "Connection Failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } $data = [ @@ -484,7 +492,9 @@ class HotspotController extends Controller { $items = $API->comm("/ip/hotspot/ip-binding/print"); $API->disconnect(); } else { - $error = "Connection Failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } $data = [ @@ -606,7 +616,9 @@ class HotspotController extends Controller { $items = $API->comm("/ip/hotspot/walled-garden/ip/print"); $API->disconnect(); } else { - $error = "Connection Failed to " . $creds['ip']; + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } $data = [ @@ -837,8 +849,9 @@ class HotspotController extends Controller { $templateContent = $tpl['content']; $viewName = 'print/custom'; } else { - // Fallback if ID invalid - $currentTemplate = 'default'; + \App\Helpers\FlashHelper::set('error', 'Template Not Found', 'The selected print template could not be found.'); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/hotspot/users')); + exit; } } diff --git a/app/Controllers/LogController.php b/app/Controllers/LogController.php index e9b55fa..bc14850 100644 --- a/app/Controllers/LogController.php +++ b/app/Controllers/LogController.php @@ -44,7 +44,10 @@ class LogController extends Controller $logs = array_reverse($logs); } - $API->disconnect(); + } else { + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $config['ip_address']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } return $this->view('reports/user_log', [ diff --git a/app/Controllers/ProfileController.php b/app/Controllers/ProfileController.php index 03a16d5..9eec493 100644 --- a/app/Controllers/ProfileController.php +++ b/app/Controllers/ProfileController.php @@ -21,6 +21,21 @@ class ProfileController extends Controller // Use default port 8728 if not specified if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { $profiles = $API->comm('/ip/hotspot/user/profile/print'); + + // Fetch Pools & Queues for the Modal Form + $pools = $API->comm('/ip/pool/print'); + $simple = $API->comm('/queue/simple/print'); + $tree = $API->comm('/queue/tree/print'); + + $queues = []; + foreach ($simple as $q) { + if(isset($q['name'])) $queues[] = $q['name']; + } + foreach ($tree as $q) { + if(isset($q['name'])) $queues[] = $q['name']; + } + sort($queues); + $API->disconnect(); // Process profiles to add metadata from on-login script @@ -33,15 +48,14 @@ class ProfileController extends Controller $this->view('hotspot/profiles/index', [ 'session' => $session, 'profiles' => $profiles, + 'pools' => $pools, + 'queues' => $queues, 'title' => 'User Profiles' ]); } else { - $this->view('hotspot/profiles/index', [ - 'session' => $session, - 'profiles' => [], - 'error' => 'Connection Failed to ' . $creds['ip'], - 'title' => 'User Profiles' - ]); + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/dashboard')); + exit; } } diff --git a/app/Controllers/PublicStatusController.php b/app/Controllers/PublicStatusController.php index 2209323..2c41e77 100644 --- a/app/Controllers/PublicStatusController.php +++ b/app/Controllers/PublicStatusController.php @@ -14,14 +14,9 @@ class PublicStatusController extends Controller { // View: Show Search Page public function index($session) { // Just verify session existence to display Hotspot Name + // Session verified by RouterCheckMiddleware $configModel = new Config(); $creds = $configModel->getSession($session); - - if (!$creds) { - // If session invalid, maybe show 404 or generic error - echo "Session not found."; - return; - } $data = [ 'session' => $session, @@ -92,9 +87,6 @@ class PublicStatusController extends Controller { if (!empty($user)) { $u = $user[0]; - // DEBUG: Log the user data to see raw values - error_log("Status Debug: " . json_encode($u)); - // --- SECURITY CHECK: Hide Unused Vouchers --- $uptimeRaw = $u['uptime'] ?? '0s'; $bytesIn = intval($u['bytes-in'] ?? 0); diff --git a/app/Controllers/QuickPrintController.php b/app/Controllers/QuickPrintController.php index b56e307..d5964d3 100644 --- a/app/Controllers/QuickPrintController.php +++ b/app/Controllers/QuickPrintController.php @@ -19,7 +19,14 @@ class QuickPrintController extends Controller { // Dashboard: List Cards public function index($session) { $qpModel = new QuickPrintModel(); - $packages = $qpModel->getAllBySession($session); + + $configModel = new Config(); + $creds = $configModel->getSession($session); + $routerId = $creds['id'] ?? null; + + // If no ID (Legacy), fallback to empty list or handle gracefully. + // For now, we assume ID exists as per migration plan. + $packages = $routerId ? $qpModel->getAllByRouterId($routerId) : []; $data = [ 'session' => $session, @@ -32,11 +39,12 @@ class QuickPrintController extends Controller { // List/Manage Packages (CRUD) public function manage($session) { $qpModel = new QuickPrintModel(); - $packages = $qpModel->getAllBySession($session); - - // Need profiles for the Add/Edit Modal + $configModel = new Config(); $creds = $configModel->getSession($session); + $routerId = $creds['id'] ?? null; + + $packages = $routerId ? $qpModel->getAllByRouterId($routerId) : []; $profiles = []; if ($creds) { $API = new RouterOSAPI(); @@ -63,7 +71,13 @@ class QuickPrintController extends Controller { if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; $session = $_POST['session'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + $routerId = $creds['id'] ?? 0; + $data = [ + 'router_id' => $routerId, 'session_name' => $session, 'name' => $_POST['name'] ?? 'Package', 'server' => $_POST['server'] ?? 'all', @@ -71,6 +85,7 @@ class QuickPrintController extends Controller { 'prefix' => $_POST['prefix'] ?? '', 'char_length' => $_POST['char_length'] ?? 4, 'price' => $_POST['price'] ?? 0, + 'selling_price' => $_POST['selling_price'] ?? ($_POST['price'] ?? 0), 'time_limit' => $_POST['time_limit'] ?? '', 'data_limit' => $_POST['data_limit'] ?? '', 'comment' => $_POST['comment'] ?? '', @@ -85,6 +100,40 @@ class QuickPrintController extends Controller { exit; } + // CRUD: Update + public function update() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + if (empty($id)) { + \App\Helpers\FlashHelper::set('error', 'common.error', 'toasts.error_missing_id', [], true); + header("Location: /" . $session . "/quick-print/manage"); + exit; + } + + $data = [ + 'name' => $_POST['name'] ?? 'Package', + 'profile' => $_POST['profile'] ?? 'default', + 'prefix' => $_POST['prefix'] ?? '', + 'char_length' => $_POST['char_length'] ?? 4, + 'price' => $_POST['price'] ?? 0, + 'selling_price' => $_POST['selling_price'] ?? ($_POST['price'] ?? 0), + 'time_limit' => $_POST['time_limit'] ?? '', + 'data_limit' => $_POST['data_limit'] ?? '', + 'comment' => $_POST['comment'] ?? '', + 'color' => $_POST['color'] ?? 'bg-blue-500' + ]; + + $qpModel = new QuickPrintModel(); + $qpModel->update($id, $data); // Assuming update method exists in simple JSON model + + \App\Helpers\FlashHelper::set('success', 'toasts.package_updated', 'toasts.package_updated_desc', [], true); + header("Location: /" . $session . "/quick-print/manage"); + exit; + } + // CRUD: Delete public function delete() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; @@ -158,7 +207,9 @@ class QuickPrintController extends Controller { $API->comm("/ip/hotspot/user/add", $userData); $API->disconnect(); } else { - die("Connection failed"); + \App\Helpers\FlashHelper::set('error', 'Connection Failed', 'Could not connect to router at ' . $creds['ip']); + header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/' . $session . '/quick-print/manage')); + exit; } diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index fbafe24..9bf8a7a 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -10,7 +10,7 @@ use App\Helpers\FormatHelper; class SettingsController extends Controller { public function __construct() { - Middleware::auth(); + // Auth handled by Router Middleware } public function system() { @@ -33,10 +33,6 @@ class SettingsController extends Controller { return $this->view('settings/index', ['routers' => $routers]); } - public function add() { - return $this->view('settings/form'); - } - // ... (Existing Store methods) ... public function store() { // Sanitize Session Name (Duplicate Frontend Logic) @@ -102,33 +98,7 @@ class SettingsController extends Controller { } - public function edit() { - // ID passed via query param or route param? - // Our router supports {id} but let's check how we handle it. - // Router: /settings/edit/{id} - // In Router.php, params are passed to method. - // So method signature should be edit($id) - - // Wait, Router.php passes matches as params array to invokeCallback. - // So we need to capture arguments here. - $args = func_get_args(); - $id = $args[0] ?? null; - if (!$id) { - header('Location: /settings/routers'); - exit; - } - - $configModel = new Config(); - $session = $configModel->getSessionById($id); - - if (!$session) { - header('Location: /settings/routers'); - exit; - } - - return $this->view('settings/form', ['router' => $session]); - } public function update() { $id = $_POST['id']; @@ -316,7 +286,7 @@ class SettingsController extends Controller { // Restore Logos if (isset($json['logos'])) { $logoModel = new \App\Models\Logo(); - $uploadDir = ROOT . '/public/assets/img/logos/'; + $uploadDir = ROOT . '/public/uploads/logos/'; if (!file_exists($uploadDir)) { mkdir($uploadDir, 0777, true); } @@ -341,7 +311,7 @@ class SettingsController extends Controller { ON CONFLICT(id) DO UPDATE SET name=excluded.name, path=excluded.path, type=excluded.type, size=excluded.size", [ 'id' => $logo['id'], 'name' => $logo['name'], - 'path' => '/assets/img/logos/' . $filename, + 'path' => '/uploads/logos/' . $filename, 'type' => $extension, 'size' => $logo['size'] ]); @@ -371,22 +341,24 @@ class SettingsController extends Controller { } public function uploadLogo() { - if (!isset($_FILES['logo_file'])) { + if (!isset($_FILES['logo_file']) || $_FILES['logo_file']['error'] !== UPLOAD_ERR_OK) { + \App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', 'toasts.no_file_selected', [], true); header('Location: /settings/logos'); exit; } $logoModel = new \App\Models\Logo(); try { - $logoModel->add($_FILES['logo_file']); + $result = $logoModel->add($_FILES['logo_file']); + if ($result) { + \App\Helpers\FlashHelper::set('success', 'toasts.logo_uploaded', 'toasts.logo_uploaded_desc', [], true); + } else { + \App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', 'Generic upload error', [], true); + } } catch (\Exception $e) { - // Ideally flash error message to session - // For now, redirect (logging error via debug or ignoring as per simple req) - // session_start() is implicit in Middleware usually or index - // $_SESSION['error'] = $e->getMessage(); + \App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', $e->getMessage(), [], true); } - \App\Helpers\FlashHelper::set('success', 'toasts.logo_uploaded', 'toasts.logo_uploaded_desc', [], true); header('Location: /settings/logos'); } diff --git a/app/Controllers/TemplateController.php b/app/Controllers/VoucherTemplateController.php similarity index 86% rename from app/Controllers/TemplateController.php rename to app/Controllers/VoucherTemplateController.php index a27849e..3bd8225 100644 --- a/app/Controllers/TemplateController.php +++ b/app/Controllers/VoucherTemplateController.php @@ -6,7 +6,7 @@ use App\Core\Controller; use App\Models\VoucherTemplateModel; use App\Core\Middleware; -class TemplateController extends Controller { +class VoucherTemplateController extends Controller { public function __construct() { Middleware::auth(); @@ -19,7 +19,7 @@ class TemplateController extends Controller { $data = [ 'templates' => $templates ]; - return $this->view('settings/templates/index', $data); + return $this->view('settings/voucher_templates/index', $data); } public function preview($id) { @@ -48,7 +48,7 @@ class TemplateController extends Controller { $data = [ 'logoMap' => $logoMap ]; - return $this->view('settings/templates/add', $data); // Note: add.php likely includes edit.php or is alias. View above says 'Template Editor (Shared)' + return $this->view('settings/voucher_templates/add', $data); } public function store() { @@ -62,6 +62,7 @@ class TemplateController extends Controller { // I will use 'global' for templates created in Settings. $data = [ + 'router_id' => 0, // Global templates 'session_name' => 'global', 'name' => $name, 'content' => $content @@ -71,7 +72,7 @@ class TemplateController extends Controller { $templateModel->add($data); \App\Helpers\FlashHelper::set('success', 'toasts.template_created', 'toasts.template_created_desc', ['name' => $name], true); - header("Location: /settings/templates"); + header("Location: /settings/voucher-templates"); exit; } @@ -80,7 +81,7 @@ class TemplateController extends Controller { $template = $templateModel->getById($id); if (!$template) { - header("Location: /settings/templates"); + header("Location: /settings/voucher-templates"); exit; } @@ -95,7 +96,7 @@ class TemplateController extends Controller { 'template' => $template, 'logoMap' => $logoMap ]; - return $this->view('settings/templates/edit', $data); + return $this->view('settings/voucher_templates/edit', $data); } public function update() { @@ -114,7 +115,7 @@ class TemplateController extends Controller { $templateModel->update($id, $data); \App\Helpers\FlashHelper::set('success', 'toasts.template_updated', 'toasts.template_updated_desc', ['name' => $name], true); - header("Location: /settings/templates"); + header("Location: /settings/voucher-templates"); exit; } @@ -126,7 +127,7 @@ class TemplateController extends Controller { $templateModel->delete($id); \App\Helpers\FlashHelper::set('success', 'toasts.template_deleted', 'toasts.template_deleted_desc', [], true); - header("Location: /settings/templates"); + header("Location: /settings/voucher-templates"); exit; } } diff --git a/app/Core/Console.php b/app/Core/Console.php index a15fac6..7bc3c15 100644 --- a/app/Core/Console.php +++ b/app/Core/Console.php @@ -45,7 +45,7 @@ class Console { private function printBanner() { echo "\n"; - echo self::COLOR_BOLD . " MIVO Helper " . self::COLOR_RESET . self::COLOR_GRAY . "v1.0" . self::COLOR_RESET . "\n\n"; + echo self::COLOR_BOLD . " MIVO Helper " . self::COLOR_RESET . self::COLOR_GRAY . "v1.1.0" . self::COLOR_RESET . "\n\n"; } private function commandServe($args) { diff --git a/app/Core/Migrations.php b/app/Core/Migrations.php index a2af0e6..5a82f26 100644 --- a/app/Core/Migrations.php +++ b/app/Core/Migrations.php @@ -61,6 +61,7 @@ class Migrations { // 6. Quick Prints (Voucher Printing Profiles) $pdo->exec("CREATE TABLE IF NOT EXISTS quick_prints ( id INTEGER PRIMARY KEY AUTOINCREMENT, + router_id INTEGER, session_name TEXT NOT NULL, name TEXT NOT NULL, server TEXT NOT NULL, @@ -68,6 +69,7 @@ class Migrations { prefix TEXT DEFAULT '', char_length INTEGER DEFAULT 4, price INTEGER DEFAULT 0, + selling_price INTEGER DEFAULT 0, time_limit TEXT DEFAULT '', data_limit TEXT DEFAULT '', comment TEXT DEFAULT '', @@ -79,6 +81,7 @@ class Migrations { // 7. Voucher Templates $pdo->exec("CREATE TABLE IF NOT EXISTS voucher_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, + router_id INTEGER, session_name TEXT NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL, diff --git a/app/Core/Router.php b/app/Core/Router.php index af6e1b0..437bc01 100644 --- a/app/Core/Router.php +++ b/app/Core/Router.php @@ -4,13 +4,91 @@ namespace App\Core; class Router { protected $routes = []; + protected $currentGroupMiddleware = []; + protected $lastRouteKey = null; + protected $middlewareAliases = [ + 'auth' => \App\Middleware\AuthMiddleware::class, + 'cors' => \App\Middleware\CorsMiddleware::class, + 'router.valid' => \App\Middleware\RouterCheckMiddleware::class, + ]; + + /** + * Add a GET route + */ public function get($path, $callback) { - $this->routes['GET'][$path] = $callback; + return $this->addRoute('GET', $path, $callback); } + /** + * Add a POST route + */ public function post($path, $callback) { - $this->routes['POST'][$path] = $callback; + return $this->addRoute('POST', $path, $callback); + } + + /** + * Add route to collection and return $this for chaining + */ + protected function addRoute($method, $path, $callback) { + $path = $this->normalizePath($path); + + $this->routes[$method][$path] = [ + 'callback' => $callback, + 'middleware' => $this->currentGroupMiddleware // Inherit group middleware + ]; + + $this->lastRouteKey = ['method' => $method, 'path' => $path]; + + return $this; + } + + /** + * Attach middleware to the last defined route + */ + public function middleware($names) { + if (!$this->lastRouteKey) return $this; + + $method = $this->lastRouteKey['method']; + $path = $this->lastRouteKey['path']; + + $middlewares = is_array($names) ? $names : [$names]; + + // Merge with existing middleware (from groups) + $this->routes[$method][$path]['middleware'] = array_merge( + $this->routes[$method][$path]['middleware'], + $middlewares + ); + + return $this; + } + + /** + * Define a route group with shared attributes (middleware, prefix, etc.) + */ + public function group($attributes, callable $callback) { + $previousGroupMiddleware = $this->currentGroupMiddleware; + + if (isset($attributes['middleware'])) { + $newMiddleware = is_array($attributes['middleware']) + ? $attributes['middleware'] + : [$attributes['middleware']]; + + $this->currentGroupMiddleware = array_merge( + $this->currentGroupMiddleware, + $newMiddleware + ); + } + + // Execute the callback with $this router instance + $callback($this); + + // Restore previous state + $this->currentGroupMiddleware = $previousGroupMiddleware; + } + + protected function normalizePath($path) { + return '/' . trim($path, '/'); } public function dispatch($uri, $method) { @@ -21,27 +99,24 @@ class Router { if (strpos($path, $scriptName) === 0) { $path = substr($path, strlen($scriptName)); } - $path = '/' . trim($path, '/'); + $path = $this->normalizePath($path); - // Global Install Check: Redirect if database is missing + // Global Install Check $dbPath = ROOT . '/app/Database/database.sqlite'; if (!file_exists($dbPath)) { - // Whitelist /install route and assets to prevent infinite loop if ($path !== '/install' && strpos($path, '/assets/') !== 0) { header('Location: /install'); exit; } } - // Check exact match first + // 1. Try Exact Match if (isset($this->routes[$method][$path])) { - $callback = $this->routes[$method][$path]; - return $this->invokeCallback($callback); + return $this->runRoute($this->routes[$method][$path], []); } - // Check dynamic routes - foreach ($this->routes[$method] as $route => $callback) { - // Convert route syntax to regex + // 2. Try Dynamic Routes (Regex) + foreach ($this->routes[$method] as $route => $config) { // e.g. /dashboard/{session} -> #^/dashboard/([^/]+)$# $pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([^/]+)', $route); $pattern = "#^" . $pattern . "$#"; @@ -49,13 +124,43 @@ class Router { if (preg_match($pattern, $path, $matches)) { array_shift($matches); // Remove full match $matches = array_map('urldecode', $matches); - return $this->invokeCallback($callback, $matches); + return $this->runRoute($config, $matches); } } \App\Helpers\ErrorHelper::show(404); } + protected function runRoute($routeConfig, $params) { + $callback = $routeConfig['callback']; + $middlewares = $routeConfig['middleware']; + + // Pipeline Runner + $pipeline = array_reduce( + array_reverse($middlewares), + function ($nextStack, $middlewareName) { + return function ($request) use ($nextStack, $middlewareName) { + // Resolve Middleware Class + $class = $this->middlewareAliases[$middlewareName] ?? $middlewareName; + + if (!class_exists($class)) { + throw new \Exception("Middleware class '$class' not found."); + } + + $instance = new $class(); + return $instance->handle($request, $nextStack); + }; + }, + function ($request) use ($callback, $params) { + // Final destination: The Controller + return $this->invokeCallback($callback, $params); + } + ); + + // Start the pipeline with the current request (mock object or just null/path) + return $pipeline($_SERVER); + } + protected function invokeCallback($callback, $params = []) { if (is_array($callback)) { $controller = new $callback[0](); diff --git a/app/Helpers/ErrorHelper.php b/app/Helpers/ErrorHelper.php index faf271b..72ca8fd 100644 --- a/app/Helpers/ErrorHelper.php +++ b/app/Helpers/ErrorHelper.php @@ -7,21 +7,26 @@ class ErrorHelper { public static function show($code = 404, $message = 'Page Not Found', $description = null) { http_response_code($code); - // Provide default descriptions for common codes + // Provide default translation keys for common codes if ($description === null) { switch ($code) { case 403: - $description = "You do not have permission to access this resource."; + $message = ($message === 'Page Not Found') ? 'errors.403_title' : $message; // Override default if simple + $description = "errors.403_desc"; break; case 500: - $description = "Something went wrong on our end. Please try again later."; + $message = ($message === 'Page Not Found') ? 'errors.500_title' : $message; + $description = "errors.500_desc"; break; case 503: - $description = "Service Unavailable. The server is currently unable to handle the request due to maintenance or overload."; + $message = ($message === 'Page Not Found') ? 'errors.503_title' : $message; + $description = "errors.503_desc"; break; case 404: default: - $description = "The page you are looking for might have been removed, had its name changed, or is temporarily unavailable."; + // If message is generic default, use key + if ($message === 'Page Not Found') $message = 'errors.404_title'; + $description = "errors.404_desc"; break; } } diff --git a/app/Middleware/AuthMiddleware.php b/app/Middleware/AuthMiddleware.php new file mode 100644 index 0000000..bf68fd0 --- /dev/null +++ b/app/Middleware/AuthMiddleware.php @@ -0,0 +1,15 @@ +query("SELECT * FROM api_cors WHERE origin = ? OR origin = '*' LIMIT 1", [$origin]); + $rule = $stmt->fetch(); + + if ($rule) { + header("Access-Control-Allow-Origin: " . ($rule['origin'] === '*' ? '*' : $origin)); + + $methods = json_decode($rule['methods'], true) ?: ['GET', 'POST']; + header("Access-Control-Allow-Methods: " . implode(', ', $methods)); + + $headers = json_decode($rule['headers'], true) ?: ['*']; + header("Access-Control-Allow-Headers: " . implode(', ', $headers)); + + header("Access-Control-Max-Age: " . ($rule['max_age'] ?? 3600)); + + // Handle preflight requests + if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + exit(); + } + } + } + + return $next($request); + } +} diff --git a/app/Middleware/MiddlewareInterface.php b/app/Middleware/MiddlewareInterface.php new file mode 100644 index 0000000..de78b41 --- /dev/null +++ b/app/Middleware/MiddlewareInterface.php @@ -0,0 +1,7 @@ +getSession($session)) { + // Router NOT FOUND + \App\Helpers\ErrorHelper::show(404, 'errors.router_not_found_title', 'errors.router_not_found_desc'); + } + } + + return $next($request); + } +} diff --git a/app/Models/Config.php b/app/Models/Config.php index 4a4f3da..e7d2fbc 100644 --- a/app/Models/Config.php +++ b/app/Models/Config.php @@ -20,6 +20,7 @@ class Config { if ($router) { return [ + 'id' => $router['id'], 'ip' => $router['ip_address'], 'ip_address' => $router['ip_address'], // Alias 'user' => $router['username'], diff --git a/app/Models/Logo.php b/app/Models/Logo.php index 10caa49..86e253c 100644 --- a/app/Models/Logo.php +++ b/app/Models/Logo.php @@ -74,7 +74,7 @@ class Logo { $exists = $this->getById($id); } while ($exists); - $uploadDir = ROOT . '/public/assets/img/logos/'; + $uploadDir = ROOT . '/public/uploads/logos/'; if (!file_exists($uploadDir)) { mkdir($uploadDir, 0777, true); } @@ -86,7 +86,7 @@ class Logo { $this->db->query("INSERT INTO {$this->table} (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size)", [ 'id' => $id, 'name' => $file['name'], - 'path' => '/assets/img/logos/' . $filename, + 'path' => '/uploads/logos/' . $filename, 'type' => $extension, 'size' => $file['size'] ]); @@ -98,7 +98,7 @@ class Logo { public function syncFiles() { // One-time sync: scan folder, if file not in DB, add it. - $logoDir = ROOT . '/public/assets/img/logos/'; + $logoDir = ROOT . '/public/uploads/logos/'; if (!file_exists($logoDir)) return; $files = []; @@ -112,7 +112,7 @@ class Logo { $extension = pathinfo($filename, PATHINFO_EXTENSION); // Check if file is registered (maybe by path match) - $webPath = '/assets/img/logos/' . $filename; + $webPath = '/uploads/logos/' . $filename; $stmt = $this->db->query("SELECT COUNT(*) FROM {$this->table} WHERE path = :path", ['path' => $webPath]); if ($stmt->fetchColumn() == 0) { diff --git a/app/Models/QuickPrintModel.php b/app/Models/QuickPrintModel.php index 4437c4e..46c3671 100644 --- a/app/Models/QuickPrintModel.php +++ b/app/Models/QuickPrintModel.php @@ -6,9 +6,9 @@ use App\Core\Database; class QuickPrintModel { - public function getAllBySession($sessionName) { + public function getAllByRouterId($routerId) { $db = Database::getInstance(); - $stmt = $db->query("SELECT * FROM quick_prints WHERE session_name = ?", [$sessionName]); + $stmt = $db->query("SELECT * FROM quick_prints WHERE router_id = ?", [$routerId]); return $stmt->fetchAll(); } @@ -20,17 +20,22 @@ class QuickPrintModel { public function add($data) { $db = Database::getInstance(); - $sql = "INSERT INTO quick_prints (session_name, name, server, profile, prefix, char_length, price, time_limit, data_limit, comment, color) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + // Insert router_id. session_name is kept for legacy/redundancy if needed, or we can drop it. + // Let's write both for now to be safe during transition, or user requirement "diubah saja" implies replacement using ID. + // But the table still has session_name column (we added router_id, didn't drop session_name). + $sql = "INSERT INTO quick_prints (router_id, session_name, name, server, profile, prefix, char_length, price, selling_price, time_limit, data_limit, comment, color) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; return $db->query($sql, [ - $data['session_name'], + $data['router_id'], + $data['session_name'], // Keep filling it for now $data['name'], - $data['server'], + $data['server'] ?? 'all', $data['profile'], $data['prefix'] ?? '', $data['char_length'] ?? 4, $data['price'] ?? 0, + $data['selling_price'] ?? ($data['price'] ?? 0), $data['time_limit'] ?? '', $data['data_limit'] ?? '', $data['comment'] ?? '', @@ -40,15 +45,15 @@ class QuickPrintModel { public function update($id, $data) { $db = Database::getInstance(); - $sql = "UPDATE quick_prints SET name=?, server=?, profile=?, prefix=?, char_length=?, price=?, time_limit=?, data_limit=?, comment=?, color=?, updated_at=CURRENT_TIMESTAMP WHERE id=?"; + $sql = "UPDATE quick_prints SET name=?, profile=?, prefix=?, char_length=?, price=?, selling_price=?, time_limit=?, data_limit=?, comment=?, color=?, updated_at=CURRENT_TIMESTAMP WHERE id=?"; return $db->query($sql, [ $data['name'], - $data['server'], $data['profile'], $data['prefix'] ?? '', $data['char_length'] ?? 4, $data['price'] ?? 0, + $data['selling_price'] ?? ($data['price'] ?? 0), $data['time_limit'] ?? '', $data['data_limit'] ?? '', $data['comment'] ?? '', diff --git a/app/Models/VoucherTemplateModel.php b/app/Models/VoucherTemplateModel.php index 6c30b9f..6f6cd3c 100644 --- a/app/Models/VoucherTemplateModel.php +++ b/app/Models/VoucherTemplateModel.php @@ -12,10 +12,9 @@ class VoucherTemplateModel { return $stmt->fetchAll(); } - public function getBySession($sessionName) { - // Templates can be global or session specific, but allow session filtering + public function getAllByRouterId($routerId) { $db = Database::getInstance(); - $stmt = $db->query("SELECT * FROM voucher_templates WHERE session_name = ? OR session_name = 'global'", [$sessionName]); + $stmt = $db->query("SELECT * FROM voucher_templates WHERE router_id = ?", [$routerId]); return $stmt->fetchAll(); } @@ -27,8 +26,9 @@ class VoucherTemplateModel { public function add($data) { $db = Database::getInstance(); - $sql = "INSERT INTO voucher_templates (session_name, name, content) VALUES (?, ?, ?)"; + $sql = "INSERT INTO voucher_templates (router_id, session_name, name, content) VALUES (?, ?, ?, ?)"; return $db->query($sql, [ + $data['router_id'], $data['session_name'], $data['name'], $data['content'] diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php index c95c530..cf408e2 100644 --- a/app/Views/dashboard.php +++ b/app/Views/dashboard.php @@ -322,7 +322,8 @@ require_once ROOT . '/app/Views/layouts/header_main.php'; // Init fetchInterfaces().then(() => { // Start Polling after interfaces loaded - setInterval(fetchTraffic, 5000); // Every 5 seconds + const reloadInterval = ; // Convert sec to ms + setInterval(fetchTraffic, reloadInterval); fetchTraffic(); }); }); diff --git a/app/Views/errors/default.php b/app/Views/errors/default.php index 181af78..930d727 100644 --- a/app/Views/errors/default.php +++ b/app/Views/errors/default.php @@ -16,17 +16,21 @@ require_once ROOT . '/app/Views/layouts/header_main.php';

-

-

+ +

> + +

+ +

>

- + Return Home -
diff --git a/app/Views/hotspot/profiles/add.php b/app/Views/hotspot/profiles/add.php deleted file mode 100644 index ec04793..0000000 --- a/app/Views/hotspot/profiles/add.php +++ /dev/null @@ -1,236 +0,0 @@ - - -
-
-
-

Add Profile

-

Create a new hotspot user profile for:

-
- - Back - -
- -
- -
-
-

-
- -
- New Profile Settings -

- -
- - - -
-

General

- - -
- - -
- - -
-
- - -
-
- -
- - - - -
-
-
-
- - -
-

Limits & Queues

- -
- -
- -
- - - - -
-
- - -
- - -
-
-
- - -
-

Pricing & Validity

- - -
- - -

Action when validity expires.

-
- - - - - -
-
- -
- - - - -
-
-
- -
- - - - -
-
-
- - -
- - -

Lock user to one specific MAC address.

-
-
- -
- Cancel - -
-
-
-
- - -
-
-
-

- - Quick Tips -

-
    -
  • - - Rate Limit: Rx/Tx (Upload/Download). Example: 512k/1M -
  • -
  • - - Expired Mode: Select 'Remove' or 'Notice' to enable Validity. -
  • -
  • - - Parent Queue: Assigns users to a specific parent queue for bandwidth management. -
  • -
-
-
-
-
-
- - - diff --git a/app/Views/hotspot/profiles/edit.php b/app/Views/hotspot/profiles/edit.php deleted file mode 100644 index 080f490..0000000 --- a/app/Views/hotspot/profiles/edit.php +++ /dev/null @@ -1,241 +0,0 @@ - - -
-
-
-

Edit Profile

-

"}'>Edit hotspot user profile:

-
- - Back - -
- -
- -
-
-

-
- -
- Edit Profile -

- -
- - - - -
-

General

- - -
- - -
- - -
-
- - -
-
- -
- - - - -
-
-
-
- - -
-

Limits & Queues

- -
- -
- -
- - - - -
-
- - -
- - -
-
-
- - -
-

Pricing & Validity

- - -
- - - -

Action when validity expires.

-
- - - - - -
-
- -
- - - - -
-
-
- -
- - - - -
-
-
- - -
- - - -

Lock user to one specific MAC address.

-
-
- -
- Cancel - -
-
-
-
- - -
-
-
-

- - Quick Tips -

-
    -
  • - - Rate Limit: Rx/Tx (Upload/Download). Example: 512k/1M -
  • -
  • - - Expired Mode: Select 'Remove' or 'Notice' to enable Validity. -
  • -
  • - - Parent Queue: Assigns users to a specific parent queue for bandwidth management. -
  • -
-
-
-
-
-
- - - diff --git a/app/Views/hotspot/profiles/index.php b/app/Views/hotspot/profiles/index.php index 1b9f68d..1796e5d 100644 --- a/app/Views/hotspot/profiles/index.php +++ b/app/Views/hotspot/profiles/index.php @@ -22,9 +22,9 @@ sort($uniqueModes); Dashboard - + @@ -79,8 +79,21 @@ sort($uniqueModes); - @@ -89,9 +102,9 @@ sort($uniqueModes);
- +
@@ -129,9 +142,9 @@ sort($uniqueModes);
- +
@@ -238,7 +251,7 @@ sort($uniqueModes); update() { this.filteredRows = this.allRows.filter(row => { - const name = row.dataset.name || ''; + const name = row.dataset.searchName || ''; const mode = row.dataset.mode || ''; if (this.filters.search && !name.includes(this.filters.search)) return false; @@ -307,5 +320,201 @@ sort($uniqueModes); const rows = document.querySelectorAll('.table-row-item'); new TableManager(rows, 10); - }); + }); + + function openProfileModal(mode, btn = null) { + const template = document.getElementById('profile-form-template').innerHTML; + + let title = window.i18n ? window.i18n.t('hotspot_profiles.form.add_title') : 'Add Profile'; + let saveBtn = window.i18n ? window.i18n.t('common.save') : 'Save'; + + if (mode === 'edit') { + title = window.i18n ? window.i18n.t('hotspot_profiles.form.edit_title') : 'Edit Profile'; + saveBtn = window.i18n ? window.i18n.t('common.forms.save_changes') : 'Save Changes'; + } + + const preConfirmFn = () => { + const form = Swal.getHtmlContainer().querySelector('form'); + if(form.reportValidity()) { + form.submit(); + return true; + } + return false; + }; + + const onOpenedFn = (popup) => { + const form = popup.querySelector('form'); + + // Validity Toggle Logic for Modal + const modeSelect = form.querySelector('#expired-mode'); + const validityGroup = form.querySelector('#validity-group'); + + function toggleValidity() { + if (!modeSelect || !validityGroup) return; + if (modeSelect.value === 'none') { + validityGroup.classList.add('hidden'); + } else { + validityGroup.classList.remove('hidden'); + } + } + + if (modeSelect) { + modeSelect.addEventListener('change', toggleValidity); + } + + if (mode === 'edit' && btn) { + const row = btn.closest('tr'); + + form.action = "//hotspot/profile/update"; + + // Populate Hidden ID + const idInput = form.querySelector('#form-id'); + idInput.disabled = false; + idInput.value = row.dataset.id; + + // Populate Fields + form.querySelector('[name="name"]').value = row.dataset.name || ''; + form.querySelector('[name="shared-users"]').value = row.dataset.sharedUsers || '1'; + form.querySelector('[name="rate-limit"]').value = row.dataset.rateLimit || ''; + + // Selects + if(form.querySelector('[name="address-pool"]')) form.querySelector('[name="address-pool"]').value = row.dataset.addressPool; + if(form.querySelector('[name="parent-queue"]')) form.querySelector('[name="parent-queue"]').value = row.dataset.parentQueue; + if(form.querySelector('[name="expired_mode"]')) form.querySelector('[name="expired_mode"]').value = row.dataset.expiredMode; + if(form.querySelector('[name="lock_user"]')) form.querySelector('[name="lock_user"]').value = row.dataset.lockUser; + + // Validity + form.querySelector('[name="validity_d"]').value = row.dataset.valD || ''; + form.querySelector('[name="validity_h"]').value = row.dataset.valH || ''; + form.querySelector('[name="validity_m"]').value = row.dataset.valM || ''; + + // Prices + form.querySelector('[name="price"]').value = row.dataset.price || ''; + form.querySelector('[name="selling_price"]').value = row.dataset.sellingPrice || ''; + + // Initial Toggle Check + toggleValidity(); + } + }; + + Mivo.modal.form(title, template, saveBtn, preConfirmFn, onOpenedFn, 'swal-wide'); + } + + diff --git a/app/Views/hotspot/users/add.php b/app/Views/hotspot/users/add.php deleted file mode 100644 index 2f0b50a..0000000 --- a/app/Views/hotspot/users/add.php +++ /dev/null @@ -1,171 +0,0 @@ - - -
-
-
-

Add User

-

Generate a new voucher/user for session:

-
- - Back to List - -
- -
-
-
-

-
- -
- User Details -

- - - - -
- -
- -
- - - - -
-

Unique username for login.

-
- -
- -
- - - - -
-

Strong password for security.

-
- - -
- - - -

Profile determines speed limit and shared user policy.

-
- - -
- -
- -
- D - -
- -
- H - -
- -
- M - -
-
-

Total allowed uptime (Days, Hours, Minutes).

-
- - -
- -
-
- - - - -
-
- -
-
-

Limit data usage (0 for unlimited).

-
- - -
- -
- - - - -
-

Additional notes or contact info.

-
-
- -
- Cancel - -
- -
-
- - -
-
-

- - Quick Tips -

-
    -
  • - - Profiles define the default speed limits, session timeout, and shared users policy. -
  • -
  • - - Time Limit is the total accumulated uptime allowed for this user. -
  • -
  • - - Data Limit will override the profile's data limit settings if specified here. Set to 0 to use profile default. -
  • -
-
-
-
-
- - - diff --git a/app/Views/hotspot/users/edit.php b/app/Views/hotspot/users/edit.php deleted file mode 100644 index 518ad4c..0000000 --- a/app/Views/hotspot/users/edit.php +++ /dev/null @@ -1,134 +0,0 @@ - - - - - -
-
-

Edit Hotspot User

-

"}'>Update user details for:

-
- - - Cancel - -
- -
-
- - - -
- -
- -
-
- -
- -
-
- - -
- -
-
- -
- -
-
- - -
- - -
- - -
- - -
- - -
- -
- -
- D - -
- -
- H - -
- -
- M - -
-
-
- - -
- -
-
- - - - -
-
- -
-
-
- - -
- -
-
- -
- -
-
-
- - -
- Cancel - -
-
-
- - diff --git a/app/Views/hotspot/users/users.php b/app/Views/hotspot/users/users.php index 891b45c..d877644 100644 --- a/app/Views/hotspot/users/users.php +++ b/app/Views/hotspot/users/users.php @@ -15,6 +15,10 @@ if (!empty($users)) { } } sort($uniqueProfiles); + +// $servers is passed from controller +if (!isset($servers)) $servers = []; + sort($uniqueComments); ?> @@ -27,9 +31,9 @@ sort($uniqueComments); Dashboard - +
@@ -107,13 +111,38 @@ sort($uniqueComments); + + data-id="" + data-name="" + data-rawname="" + data-profile="" + data-comment="" + data-comment-raw="" + data-password="" + data-server="" + data-limit-uptime="" + data-limit-bytes-total=""> - +
@@ -122,19 +151,19 @@ sort($uniqueComments);
-
+
-
+
- + @@ -148,19 +177,19 @@ sort($uniqueComments); -
+
- - + + - + @@ -188,6 +217,133 @@ sort($uniqueComments);
+ + + diff --git a/app/Views/layouts/footer_main.php b/app/Views/layouts/footer_main.php index 0aede91..7b7785a 100644 --- a/app/Views/layouts/footer_main.php +++ b/app/Views/layouts/footer_main.php @@ -6,9 +6,26 @@ -