mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 13:31:56 +07:00
Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack
This commit is contained in:
74
app/Controllers/ApiController.php
Normal file
74
app/Controllers/ApiController.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
use App\Models\Config;
|
||||
use App\Helpers\EncryptionHelper;
|
||||
|
||||
class ApiController extends Controller {
|
||||
|
||||
public function getInterfaces() {
|
||||
// Only allow POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method Not Allowed']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get JSON Input
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$ip = $input['ip'] ?? '';
|
||||
$user = $input['user'] ?? '';
|
||||
$pass = $input['password'] ?? '';
|
||||
$id = $input['id'] ?? null;
|
||||
$port = $input['port'] ?? 8728; // Default port
|
||||
|
||||
// Fallback to stored password if empty and ID provided (Edit Mode)
|
||||
if (empty($pass) && !empty($id)) {
|
||||
$configModel = new Config();
|
||||
$session = $configModel->getSessionById($id);
|
||||
if ($session && !empty($session['password'])) {
|
||||
$pass = EncryptionHelper::decrypt($session['password']);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($ip) || empty($user)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'IP Address and Username are required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$api = new RouterOSAPI();
|
||||
// $api->debug = true; // Enable for debugging
|
||||
$api->port = (int)$port;
|
||||
|
||||
if ($api->connect($ip, $user, $pass)) {
|
||||
$api->write('/interface/print');
|
||||
$read = $api->read(false);
|
||||
$interfaces = $api->parseResponse($read);
|
||||
$api->disconnect();
|
||||
|
||||
$list = [];
|
||||
foreach ($interfaces as $iface) {
|
||||
if (isset($iface['name'])) {
|
||||
$list[] = $iface['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Return success
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'interfaces' => $list
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'error' => 'Connection failed. Check IP, User, Password, or connectivity.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/Controllers/AuthController.php
Normal file
43
app/Controllers/AuthController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\User;
|
||||
|
||||
class AuthController extends Controller {
|
||||
|
||||
public function showLogin() {
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
return $this->view('login');
|
||||
}
|
||||
|
||||
public function login() {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
$userModel = new User();
|
||||
$user = $userModel->attempt($username, $password);
|
||||
|
||||
if ($user) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
\App\Helpers\FlashHelper::set('success', 'Welcome Back', 'Login successful.');
|
||||
header('Location: /');
|
||||
exit;
|
||||
} else {
|
||||
\App\Helpers\FlashHelper::set('error', 'Login Failed', 'Invalid credentials');
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
session_destroy();
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
114
app/Controllers/DashboardController.php
Normal file
114
app/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
use App\Core\Middleware;
|
||||
|
||||
class DashboardController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
Middleware::auth();
|
||||
}
|
||||
|
||||
public function index($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
|
||||
if (!$creds) {
|
||||
echo "Session not found.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock Data for Demo (SQLite or Legacy)
|
||||
if ($session === 'demo') {
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'clock' => ['time' => '12:00:00', 'date' => 'jan/01/2024'],
|
||||
'resource' => [
|
||||
'board-name' => 'CHR (Demo SQLite)',
|
||||
'version' => '7.12',
|
||||
'uptime' => '1w 2d 3h',
|
||||
'cpu-load' => '15',
|
||||
'free-memory' => 1048576 * 512, // 512 MB
|
||||
'free-hdd-space' => 1048576 * 1024, // 1 GB
|
||||
],
|
||||
// ... rest of mock data
|
||||
'routerboard' => ['model' => 'x86_64'],
|
||||
'hotspot_active' => 25,
|
||||
'hotspot_users' => 150,
|
||||
'lang' => [
|
||||
'system_date_time' => 'System Date & Time',
|
||||
'uptime' => 'Uptime',
|
||||
'board_name' => 'Board Name',
|
||||
'model' => 'Model',
|
||||
'cpu_load' => 'CPU Load',
|
||||
'free_memory' => 'Free Memory',
|
||||
'free_hdd' => 'Free HDD',
|
||||
'hotspot_active' => 'Hotspot Active',
|
||||
'hotspot_users' => 'Hotspot Users',
|
||||
]
|
||||
];
|
||||
return $this->view('dashboard', $data);
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
|
||||
// Determine password: if legacy, decrypt it. If SQLite (new), assume plain for now
|
||||
// (since we just seeded 'admin' plain in setup_database.php) or decrypt if you decide to encrypt in DB.
|
||||
// For this Demo, setup_database.php inserted plain 'admin'.
|
||||
// Existing v3 passwords are encrypted.
|
||||
|
||||
$password = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password = RouterOSAPI::decrypt($password);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password)) {
|
||||
// ... API calls
|
||||
$getclock = $API->comm("/system/clock/print");
|
||||
$clock = $getclock[0] ?? [];
|
||||
|
||||
$getresource = $API->comm("/system/resource/print");
|
||||
$resource = $getresource[0] ?? [];
|
||||
|
||||
$getrouterboard = $API->comm("/system/routerboard/print");
|
||||
$routerboard = $getrouterboard[0] ?? [];
|
||||
|
||||
$counthotspotactive = $API->comm("/ip/hotspot/active/print", array("count-only" => ""));
|
||||
$countallusers = $API->comm("/ip/hotspot/user/print", array("count-only" => ""));
|
||||
|
||||
$API->disconnect();
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'clock' => $clock,
|
||||
'resource' => $resource,
|
||||
'routerboard' => $routerboard,
|
||||
'hotspot_active' => $counthotspotactive,
|
||||
'hotspot_users' => $countallusers,
|
||||
'lang' => [
|
||||
'system_date_time' => 'System Date & Time',
|
||||
'uptime' => 'Uptime',
|
||||
'board_name' => 'Board Name',
|
||||
'model' => 'Model',
|
||||
'cpu_load' => 'CPU Load',
|
||||
'free_memory' => 'Free Memory',
|
||||
'free_hdd' => 'Free HDD',
|
||||
'hotspot_active' => 'Hotspot Active',
|
||||
'hotspot_users' => 'Hotspot Users',
|
||||
'hotspot_users' => 'Hotspot Users',
|
||||
],
|
||||
'interface' => $creds['interface'] ?? 'ether1'
|
||||
];
|
||||
// Pass Users Link (Optional: could be part of layout or card link)
|
||||
// Ideally, the "Hotspot Users" card on dashboard should be clickable.
|
||||
return $this->view('dashboard', $data);
|
||||
|
||||
} else {
|
||||
echo "Connection Failed to " . $creds['ip'];
|
||||
}
|
||||
}
|
||||
}
|
||||
38
app/Controllers/DhcpController.php
Normal file
38
app/Controllers/DhcpController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
use App\Helpers\HotspotHelper;
|
||||
|
||||
class DhcpController extends Controller
|
||||
{
|
||||
public function index($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
if (!$config) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$leases = [];
|
||||
$API = new RouterOSAPI();
|
||||
$API->attempts = 1;
|
||||
$API->timeout = 3;
|
||||
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
// Fetch DHCP Leases
|
||||
$leases = $API->comm("/ip/dhcp-server/lease/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
// Add index for viewing
|
||||
return $this->view('network/dhcp', [
|
||||
'session' => $session,
|
||||
'leases' => $leases ?? []
|
||||
]);
|
||||
}
|
||||
}
|
||||
166
app/Controllers/GeneratorController.php
Normal file
166
app/Controllers/GeneratorController.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
class GeneratorController extends Controller {
|
||||
|
||||
public function index($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
|
||||
if (!$creds) {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
// Fetch Profiles for Dropdown
|
||||
$profiles = $API->comm('/ip/hotspot/user/profile/print');
|
||||
// Fetch Hotspot Servers
|
||||
$servers = $API->comm('/ip/hotspot/print');
|
||||
$API->disconnect();
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'title' => 'Generate Vouchers - ' . $session,
|
||||
'profiles' => $profiles,
|
||||
'servers' => $servers
|
||||
];
|
||||
|
||||
$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'];
|
||||
}
|
||||
}
|
||||
|
||||
public function process() {
|
||||
$session = $_POST['session'] ?? '';
|
||||
$qty = intval($_POST['qty'] ?? 1);
|
||||
$server = $_POST['server'] ?? 'all';
|
||||
$userMode = $_POST['userModel'] ?? 'up';
|
||||
$userLength = intval($_POST['userLength'] ?? 4);
|
||||
$prefix = $_POST['prefix'] ?? '';
|
||||
$char = $_POST['char'] ?? 'mix';
|
||||
$profile = $_POST['profile'] ?? '';
|
||||
$comment = $_POST['comment'] ?? '';
|
||||
|
||||
// Time Limit Logic (d, h, m)
|
||||
$timelimit_d = $_POST['timelimit_d'] ?? '';
|
||||
$timelimit_h = $_POST['timelimit_h'] ?? '';
|
||||
$timelimit_m = $_POST['timelimit_m'] ?? '';
|
||||
|
||||
$timeLimit = '';
|
||||
if ($timelimit_d != '') $timeLimit .= $timelimit_d . 'd';
|
||||
if ($timelimit_h != '') $timeLimit .= $timelimit_h . 'h';
|
||||
if ($timelimit_m != '') $timeLimit .= $timelimit_m . 'm';
|
||||
|
||||
// Data Limit Logic (Value, Unit)
|
||||
$datalimit_val = $_POST['datalimit_val'] ?? '';
|
||||
$datalimit_unit = $_POST['datalimit_unit'] ?? 'MB';
|
||||
|
||||
$dataLimit = '';
|
||||
if (!empty($datalimit_val) && is_numeric($datalimit_val)) {
|
||||
$bytes = (float)$datalimit_val;
|
||||
if ($datalimit_unit === 'GB') {
|
||||
$bytes = $bytes * 1073741824;
|
||||
} else {
|
||||
// MB
|
||||
$bytes = $bytes * 1048576;
|
||||
}
|
||||
$dataLimit = (string)round($bytes);
|
||||
}
|
||||
|
||||
if (!$session || $qty < 1 || !$profile) {
|
||||
$this->back($session);
|
||||
return;
|
||||
}
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
|
||||
// Format Comment: prefix-rand-date- comment
|
||||
// Example: up-123-12.01.26- premium
|
||||
$commentPrefix = ($userMode === 'vc') ? 'vc-' : 'up-';
|
||||
$batchId = rand(100, 999);
|
||||
$date = date('m.d.y');
|
||||
$commentBody = $comment ?: $profile;
|
||||
$finalComment = "{$commentPrefix}{$batchId}-{$date}- {$commentBody}";
|
||||
|
||||
for ($i = 0; $i < $qty; $i++) {
|
||||
$username = $prefix . $this->generateRandomString($userLength, $char);
|
||||
$password = $username;
|
||||
|
||||
if ($userMode === 'up') {
|
||||
$password = $this->generateRandomString($userLength, $char);
|
||||
}
|
||||
|
||||
$user = [
|
||||
'server' => $server,
|
||||
'profile' => $profile,
|
||||
'name' => $username,
|
||||
'password' => $password,
|
||||
'comment' => $finalComment
|
||||
];
|
||||
|
||||
if (!empty($timeLimit)) $user['limit-uptime'] = $timeLimit;
|
||||
if (!empty($dataLimit)) $user['limit-bytes-total'] = $dataLimit;
|
||||
|
||||
$API->comm("/ip/hotspot/user/add", $user);
|
||||
}
|
||||
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.vouchers_generated', 'toasts.vouchers_generated_desc', ['qty' => $qty], true);
|
||||
$this->redirect('/' . $session . '/hotspot/users');
|
||||
}
|
||||
|
||||
private function generateRandomString($length, $charType) {
|
||||
$characters = '';
|
||||
switch ($charType) {
|
||||
case 'lower':
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
break;
|
||||
case 'upper':
|
||||
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
break;
|
||||
case 'number':
|
||||
$characters = '0123456789';
|
||||
break;
|
||||
case 'uppernumber':
|
||||
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
break;
|
||||
case 'lowernumber':
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
break;
|
||||
case 'mix':
|
||||
default:
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
break;
|
||||
}
|
||||
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[rand(0, strlen($characters) - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
private function back($session) {
|
||||
$this->redirect('/' . $session . '/hotspot/generate');
|
||||
}
|
||||
}
|
||||
34
app/Controllers/HomeController.php
Normal file
34
app/Controllers/HomeController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
|
||||
class HomeController extends Controller {
|
||||
public function __construct() {
|
||||
\App\Core\Middleware::auth();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
// Fetch real router sessions from Config model
|
||||
$config = new \App\Models\Config();
|
||||
$routers = $config->getAllSessions();
|
||||
|
||||
$data = [
|
||||
'routers' => $routers
|
||||
];
|
||||
|
||||
$this->view('home', $data);
|
||||
}
|
||||
|
||||
public function designSystem() {
|
||||
$data = ['title' => 'MIVO - Design System'];
|
||||
$this->view('design_system', $data);
|
||||
}
|
||||
|
||||
public function testAlert() {
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.test_alert', 'toasts.test_alert_desc', [], true);
|
||||
header("Location: /");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
864
app/Controllers/HotspotController.php
Normal file
864
app/Controllers/HotspotController.php
Normal file
@@ -0,0 +1,864 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
use App\Models\VoucherTemplateModel;
|
||||
use App\Core\Middleware;
|
||||
|
||||
class HotspotController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
Middleware::auth();
|
||||
}
|
||||
|
||||
public function index($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
|
||||
if (!$creds) {
|
||||
echo "Session not found.";
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $session; // For view context
|
||||
$users = [];
|
||||
$error = null;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
//$API->debug = true; // Enable for debugging
|
||||
|
||||
// Decrypt password if from SQLite
|
||||
$password = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password = RouterOSAPI::decrypt($password);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password)) {
|
||||
// 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");
|
||||
|
||||
$API->disconnect();
|
||||
} else {
|
||||
$error = "Connection Failed to " . $creds['ip'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'users' => $users,
|
||||
'error' => $error
|
||||
];
|
||||
|
||||
return $this->view('hotspot/users/users', $data);
|
||||
}
|
||||
|
||||
public function add($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return; // Should handle error better
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
|
||||
$password = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password = RouterOSAPI::decrypt($password);
|
||||
}
|
||||
|
||||
$profiles = [];
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password)) {
|
||||
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'profiles' => $profiles
|
||||
];
|
||||
|
||||
return $this->view('hotspot/users/add', $data);
|
||||
}
|
||||
|
||||
public function store() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$name = $_POST['name'] ?? '';
|
||||
$password_user = $_POST['password'] ?? '';
|
||||
$profile = $_POST['profile'] ?? 'default';
|
||||
$comment = $_POST['comment'] ?? '';
|
||||
|
||||
// Time Limit Logic (d, h, m)
|
||||
$timelimit_d = $_POST['timelimit_d'] ?? '';
|
||||
$timelimit_h = $_POST['timelimit_h'] ?? '';
|
||||
$timelimit_m = $_POST['timelimit_m'] ?? '';
|
||||
|
||||
$timelimit = '';
|
||||
if ($timelimit_d != '') $timelimit .= $timelimit_d . 'd';
|
||||
if ($timelimit_h != '') $timelimit .= $timelimit_h . 'h';
|
||||
if ($timelimit_m != '') $timelimit .= $timelimit_m . 'm';
|
||||
|
||||
// Data Limit Logic (Value, Unit)
|
||||
$datalimit_val = $_POST['datalimit_val'] ?? '';
|
||||
$datalimit_unit = $_POST['datalimit_unit'] ?? 'MB';
|
||||
|
||||
$datalimit = '';
|
||||
if (!empty($datalimit_val) && is_numeric($datalimit_val)) {
|
||||
$bytes = (int)$datalimit_val;
|
||||
if ($datalimit_unit === 'GB') {
|
||||
$bytes = $bytes * 1073741824;
|
||||
} else {
|
||||
// MB
|
||||
$bytes = $bytes * 1048576;
|
||||
}
|
||||
$datalimit = (string)$bytes;
|
||||
}
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
|
||||
$userData = [
|
||||
'name' => $name,
|
||||
'password' => $password_user,
|
||||
'profile' => $profile,
|
||||
'comment' => $comment
|
||||
];
|
||||
|
||||
if(!empty($timelimit)) $userData['limit-uptime'] = $timelimit;
|
||||
if(!empty($datalimit)) $userData['limit-bytes-total'] = $datalimit;
|
||||
|
||||
$API->comm("/ip/hotspot/user/add", $userData);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.user_added', 'toasts.user_added_desc', ['name' => $name], true);
|
||||
header("Location: /" . $session . "/hotspot/users");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$rawId = $_POST['id'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
// Handle Multiple IDs (comma separated)
|
||||
$ids = explode(',', $rawId);
|
||||
foreach ($ids as $id) {
|
||||
$id = trim($id);
|
||||
if (!empty($id)) {
|
||||
// 1. Get Username first (to delete scheduler)
|
||||
$user = $API->comm("/ip/hotspot/user/print", [
|
||||
"?.id" => $id
|
||||
]);
|
||||
|
||||
if (!empty($user) && isset($user[0]['name'])) {
|
||||
$username = $user[0]['name'];
|
||||
|
||||
// 2. Remove User
|
||||
$API->comm("/ip/hotspot/user/remove", [".id" => $id]);
|
||||
|
||||
// 3. Remove Scheduler (Ghost Cleanup)
|
||||
// Check if scheduler exists with same name as user
|
||||
$schedules = $API->comm("/system/scheduler/print", [
|
||||
"?name" => $username
|
||||
]);
|
||||
|
||||
if(!empty($schedules)) {
|
||||
// Loop just in case multiple matches (unlikely if unique name)
|
||||
foreach($schedules as $sch) {
|
||||
$API->comm("/system/scheduler/remove", [
|
||||
".id" => $sch['.id']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.user_deleted', 'toasts.user_deleted_desc', [], true);
|
||||
header("Location: /" . $session . "/hotspot/users");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function edit($session, $id) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
$user = null;
|
||||
$profiles = [];
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
// Fetch specific user
|
||||
$userRequest = $API->comm("/ip/hotspot/user/print", [
|
||||
"?.id" => $id
|
||||
]);
|
||||
if (!empty($userRequest)) {
|
||||
$user = $userRequest[0];
|
||||
|
||||
// Parse Time Limit (limit-uptime) e.g. 1d04:00:00 or 30d
|
||||
// Mikrotik uptime format varies. Safe parse:
|
||||
// Regex for Xd, Xh, Xm? NO, Mikrotik returns "4w2d" or "10:00:00" (h:m:s)
|
||||
// Or simple seconds if raw? Print usually returns formatted.
|
||||
// Let's defer to a helper or simple parsing.
|
||||
// Actually standard format: 1d 04:00:00 or 1h30m.
|
||||
// Let's try simple regex extraction.
|
||||
$t_d = ''; $t_h = ''; $t_m = '';
|
||||
$uptime = $user['limit-uptime'] ?? '';
|
||||
if ($uptime) {
|
||||
if (preg_match('/(\d+)d/', $uptime, $m)) $t_d = $m[1];
|
||||
if (preg_match('/(\d+)h/', $uptime, $m)) $t_h = $m[1];
|
||||
if (preg_match('/(\d+)m/', $uptime, $m)) $t_m = $m[1];
|
||||
// Handle H:M:S format (e.g. 04:00:00) if no 'h'/'m' chars?
|
||||
// Mikrotik CLI `print` implies "1d04:00:00". API might return "1d04:00:00".
|
||||
// If so, 04 is hours.
|
||||
// Simple parse if regex failed?
|
||||
// Let's assume standard XdXhXm usage for now based on Add form.
|
||||
}
|
||||
$user['time_d'] = $t_d;
|
||||
$user['time_h'] = $t_h;
|
||||
$user['time_m'] = $t_m;
|
||||
|
||||
// Parse Data Limit (limit-bytes-total)
|
||||
$bytes = $user['limit-bytes-total'] ?? 0;
|
||||
$d_val = ''; $d_unit = 'MB';
|
||||
if ($bytes > 0) {
|
||||
if ($bytes >= 1073741824) {
|
||||
$d_val = round($bytes / 1073741824, 2);
|
||||
$d_unit = 'GB';
|
||||
} else {
|
||||
$d_val = round($bytes / 1048576, 2);
|
||||
$d_unit = 'MB';
|
||||
}
|
||||
}
|
||||
$user['data_val'] = $d_val;
|
||||
$user['data_unit'] = $d_unit;
|
||||
}
|
||||
|
||||
// Fetch Profiles
|
||||
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
||||
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
header("Location: /" . $session . "/hotspot/users");
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'user' => $user,
|
||||
'profiles' => $profiles
|
||||
];
|
||||
|
||||
return $this->view('hotspot/users/edit', $data);
|
||||
}
|
||||
|
||||
public function update() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
$name = $_POST['name'] ?? '';
|
||||
$password_user = $_POST['password'] ?? '';
|
||||
$profile = $_POST['profile'] ?? '';
|
||||
$comment = $_POST['comment'] ?? '';
|
||||
$server = $_POST['server'] ?? 'all';
|
||||
|
||||
// Time Limit Logic (d, h, m)
|
||||
$timelimit_d = $_POST['timelimit_d'] ?? '';
|
||||
$timelimit_h = $_POST['timelimit_h'] ?? '';
|
||||
$timelimit_m = $_POST['timelimit_m'] ?? '';
|
||||
|
||||
$timelimit = '';
|
||||
if ($timelimit_d != '') $timelimit .= $timelimit_d . 'd';
|
||||
if ($timelimit_h != '') $timelimit .= $timelimit_h . 'h';
|
||||
if ($timelimit_m != '') $timelimit .= $timelimit_m . 'm';
|
||||
|
||||
// Data Limit Logic (Value, Unit)
|
||||
$datalimit_val = $_POST['datalimit_val'] ?? '';
|
||||
$datalimit_unit = $_POST['datalimit_unit'] ?? 'MB';
|
||||
|
||||
$datalimit = '0';
|
||||
if (!empty($datalimit_val) && is_numeric($datalimit_val)) {
|
||||
$bytes = (float)$datalimit_val; // float to handle decimals before calc
|
||||
if ($datalimit_unit === 'GB') {
|
||||
$bytes = $bytes * 1073741824;
|
||||
} else {
|
||||
// MB
|
||||
$bytes = $bytes * 1048576;
|
||||
}
|
||||
$datalimit = (string)round($bytes);
|
||||
}
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
|
||||
$userData = [
|
||||
'.id' => $id,
|
||||
'name' => $name,
|
||||
'password' => $password_user,
|
||||
'profile' => $profile,
|
||||
'comment' => $comment,
|
||||
'server' => $server
|
||||
];
|
||||
|
||||
if(!empty($timelimit)) $userData['limit-uptime'] = $timelimit;
|
||||
else $userData['limit-uptime'] = '0s'; // Reset if empty
|
||||
|
||||
// Always set if calculated, 0 resets it.
|
||||
$userData['limit-bytes-total'] = $datalimit;
|
||||
|
||||
$API->comm("/ip/hotspot/user/set", $userData);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.user_updated', 'toasts.user_updated_desc', ['name' => $name], true);
|
||||
header("Location: /" . $session . "/hotspot/users");
|
||||
exit;
|
||||
}
|
||||
public function active($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
header("Location: /");
|
||||
exit;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$error = null;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$items = $API->comm("/ip/hotspot/active/print");
|
||||
$API->disconnect();
|
||||
} else {
|
||||
$error = "Connection Failed to " . $creds['ip'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'items' => $items,
|
||||
'error' => $error
|
||||
];
|
||||
|
||||
return $this->view('status/active', $data);
|
||||
}
|
||||
|
||||
public function removeActive() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$API->comm("/ip/hotspot/active/remove", [".id" => $id]);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.session_removed', 'toasts.session_removed_desc', [], true);
|
||||
header("Location: /" . $session . "/hotspot/active");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function hosts($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
header("Location: /");
|
||||
exit;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$error = null;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$items = $API->comm("/ip/hotspot/host/print");
|
||||
$API->disconnect();
|
||||
} else {
|
||||
$error = "Connection Failed to " . $creds['ip'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'items' => $items,
|
||||
'error' => $error
|
||||
];
|
||||
|
||||
return $this->view('status/hosts', $data);
|
||||
}
|
||||
|
||||
public function bindings($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
header("Location: /");
|
||||
exit;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$error = null;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$items = $API->comm("/ip/hotspot/ip-binding/print");
|
||||
$API->disconnect();
|
||||
} else {
|
||||
$error = "Connection Failed to " . $creds['ip'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'items' => $items,
|
||||
'error' => $error
|
||||
];
|
||||
|
||||
return $this->view('security/bindings', $data);
|
||||
}
|
||||
|
||||
public function storeBinding() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$mac = $_POST['mac'] ?? '';
|
||||
$address = $_POST['address'] ?? '';
|
||||
$toAddress = $_POST['to_address'] ?? '';
|
||||
$server = $_POST['server'] ?? 'all';
|
||||
$type = $_POST['type'] ?? 'regular';
|
||||
$comment = $_POST['comment'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$data = [
|
||||
'mac-address' => $mac,
|
||||
'type' => $type,
|
||||
'comment' => $comment,
|
||||
'server' => $server
|
||||
];
|
||||
if(!empty($address)) $data['address'] = $address;
|
||||
if(!empty($toAddress)) $data['to-address'] = $toAddress;
|
||||
|
||||
$API->comm("/ip/hotspot/ip-binding/add", $data);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.binding_added', 'toasts.binding_added_desc', [], true);
|
||||
header("Location: /" . $session . "/hotspot/bindings");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function removeBinding() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$API->comm("/ip/hotspot/ip-binding/remove", [".id" => $id]);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.binding_removed', 'toasts.binding_removed_desc', [], true);
|
||||
header("Location: /" . $session . "/hotspot/bindings");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function walledGarden($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
header("Location: /");
|
||||
exit;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$error = null;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$items = $API->comm("/ip/hotspot/walled-garden/ip/print");
|
||||
// Standard walled garden print usually involves /ip/hotspot/walled-garden/ip/print for IP based or just /ip/hotspot/walled-garden/print
|
||||
// Let's use /ip/hotspot/walled-garden/ip/print as general walled garden often implies IP based rules in modern RouterOS or just walled-garden
|
||||
// Actually, usually there is /ip/hotspot/walled-garden (Dst Host, etc) and /ip/hotspot/walled-garden/ip (Dst Address, etc)
|
||||
// Mikhmon v3 usually merges them or uses one.
|
||||
// Let's check typical Mikhmon usage. Usually "Walled Garden" uses `/ip/hotspot/walled-garden/print` (which captures domains) and IP List uses `/ip/hotspot/walled-garden/ip/print`.
|
||||
// My view lists Dst Host / IP.
|
||||
// Let's fetch BOTH and merge, or just one.
|
||||
// For now, let's target `/ip/hotspot/walled-garden/ip/print` as it allows protocol, port, dst-address, dst-host (in newer ROS?).
|
||||
// Wait, `/ip/hotspot/walled-garden/print` allows `dst-host`.
|
||||
// `/ip/hotspot/walled-garden/ip/print` allows `dst-address`.
|
||||
// I'll stick to `/ip/hotspot/walled-garden/ip/print` for now as it seems more robust for IP rules, but domains need `walled-garden/print`.
|
||||
// Actually, let's look at `walled_garden.php`. It handles `dst-host` or `dst-address`.
|
||||
// I will use `/ip/hotspot/walled-garden/ip/print` which is "Walled Garden IP List". This is usually what people mean by "Walled Garden" for banking apps etc (IP ranges or strict definitions).
|
||||
// BUT domain bypasses are in `/ip/hotspot/walled-garden/print`.
|
||||
// Let's try to fetch `/ip/hotspot/walled-garden/ip/print` first.
|
||||
|
||||
$items = $API->comm("/ip/hotspot/walled-garden/ip/print");
|
||||
$API->disconnect();
|
||||
} else {
|
||||
$error = "Connection Failed to " . $creds['ip'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'items' => $items,
|
||||
'error' => $error
|
||||
];
|
||||
|
||||
return $this->view('security/walled_garden', $data);
|
||||
}
|
||||
|
||||
public function storeWalledGarden() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$dstHost = $_POST['dst_host'] ?? '';
|
||||
$dstAddress = $_POST['dst_address'] ?? '';
|
||||
$protocol = $_POST['protocol'] ?? '';
|
||||
$dstPort = $_POST['dst_port'] ?? '';
|
||||
$action = $_POST['action'] ?? 'allow';
|
||||
$server = $_POST['server'] ?? 'all';
|
||||
$comment = $_POST['comment'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$data = [
|
||||
'action' => $action,
|
||||
'server' => $server,
|
||||
'comment' => $comment
|
||||
];
|
||||
|
||||
// If dst-host is present, we might need to use /ip/hotspot/walled-garden/add instead of /ip/.../ip/add?
|
||||
// RouterOS distinguishes them. active.php shows I used `walled-garden/ip/print`.
|
||||
// If user enters dst-host, it usually goes to `walled-garden`. If dst-address, `walled-garden/ip`.
|
||||
// This is complex. Let's assume we are adding to `walled-garden/ip` for now which supports protocol/port/dst-address but NOT dst-host typically (older ROS).
|
||||
// Actually, newer ROS might merge.
|
||||
// Let's assume standard behavior:
|
||||
// If dst-host is provided, add to `/ip/hotspot/walled-garden/add`.
|
||||
// If dst-address is provided, add to `/ip/hotspot/walled-garden/ip/add`.
|
||||
// My View asks for BOTH?
|
||||
// Let's simplification: Check if dst_host is set.
|
||||
|
||||
$path = "/ip/hotspot/walled-garden/ip/add";
|
||||
if (!empty($dstHost)) {
|
||||
$path = "/ip/hotspot/walled-garden/add";
|
||||
$data['dst-host'] = $dstHost;
|
||||
} else {
|
||||
if(!empty($dstAddress)) $data['dst-address'] = $dstAddress;
|
||||
}
|
||||
|
||||
// Protocol and Port logic
|
||||
// Note: `walled-garden` (host) takes protocol/port too? Yes.
|
||||
if(!empty($protocol)) {
|
||||
// extract protocol name if format is "(6) tcp"
|
||||
if(preg_match('/\)\s*(\w+)/', $protocol, $m)) $protocol = $m[1];
|
||||
$data['protocol'] = $protocol;
|
||||
}
|
||||
if(!empty($dstPort)) $data['dst-port'] = $dstPort;
|
||||
|
||||
$API->comm($path, $data);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.rule_added', 'toasts.rule_added_desc', [], true);
|
||||
header("Location: /" . $session . "/hotspot/walled-garden");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function removeWalledGarden() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$API->comm("/ip/hotspot/walled-garden/ip/remove", [".id" => $id]);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.rule_removed', 'toasts.rule_removed_desc', [], true);
|
||||
header("Location: /" . $session . "/hotspot/walled-garden");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Print Single User
|
||||
public function printUser($session, $id) {
|
||||
return $this->printBatch($session, $id);
|
||||
}
|
||||
|
||||
// Print Batch Users (Comma separated IDs)
|
||||
public function printBatchActions($session) {
|
||||
$ids = $_GET['ids'] ?? '';
|
||||
return $this->printBatch($session, $ids);
|
||||
}
|
||||
|
||||
// Cookies
|
||||
public function cookies($session) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return header('Location: /');
|
||||
|
||||
$cookies = [];
|
||||
$API = new RouterOSAPI();
|
||||
$API->attempts = 1;
|
||||
$API->timeout = 3;
|
||||
|
||||
if ($API->connect($creds['ip_address'], $creds['username'], $creds['password'])) {
|
||||
$cookies = $API->comm("/ip/hotspot/cookie/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
return $this->view('hotspot/cookies', [
|
||||
'session' => $session,
|
||||
'cookies' => $cookies ?? []
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeCookie() {
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($creds['ip_address'], $creds['username'], $creds['password'])) {
|
||||
$API->comm("/ip/hotspot/cookie/remove", [".id" => $id]);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.cookie_removed', 'toasts.cookie_removed_desc', [], true);
|
||||
header("Location: /$session/hotspot/cookies");
|
||||
}
|
||||
|
||||
private function printBatch($session, $rawIds) {
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) die("Session error");
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
// Handle ID List
|
||||
// IDs can be "id1,id2,id3"
|
||||
// Also Mikrotik IDs start with *, we need to ensure they are handled.
|
||||
// If passed via URL, `*` might be encoded.
|
||||
$idList = explode(',', urldecode($rawIds));
|
||||
$validUsers = [];
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
// Optimized: Fetch ALL users and filter PHP side?
|
||||
// Or fetch loop? Mikrotik API `/print` with `?.id` only supports single match usually or we need filtered print.
|
||||
// Usually `print` returning all is faster for < 1000 users than 100 calls.
|
||||
// But if we have 5000 users, we shouldn't fetch all.
|
||||
// Mikrotik API doesn't support `WHERE id IN (...)`.
|
||||
// So for batch, we might have to loop calls OR fetch all and filter if list is huge.
|
||||
// Let's loop for now as batch print is usually < 50 items.
|
||||
|
||||
foreach ($idList as $id) {
|
||||
// Ensure ID has * if missing (unlikely if coming from app logic)
|
||||
$req = $API->comm("/ip/hotspot/user/print", [
|
||||
"?.id" => $id
|
||||
]);
|
||||
if (!empty($req)) {
|
||||
$u = $req[0];
|
||||
$validUsers[] = [
|
||||
'username' => $u['name'],
|
||||
'password' => $u['password'] ?? '',
|
||||
'price' => $u['price'] ?? '',
|
||||
'validity' => $u['limit-uptime'] ?? '',
|
||||
'timelimit' => \App\Helpers\HotspotHelper::formatValidity($u['limit-uptime'] ?? ''),
|
||||
'datalimit' => \App\Helpers\HotspotHelper::formatBytes($u['limit-bytes-total'] ?? 0),
|
||||
'profile' => $u['profile'] ?? 'default',
|
||||
'comment' => $u['comment'] ?? '',
|
||||
'hotspotname' => $creds['hotspot_name'],
|
||||
'dns_name' => $creds['dns_name'],
|
||||
'login_url' => (preg_match("~^(?:f|ht)tps?://~i", $creds['dns_name']) ? $creds['dns_name'] : "http://" . $creds['dns_name']) . "/login"
|
||||
];
|
||||
}
|
||||
}
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
if (empty($validUsers)) die("No users found");
|
||||
|
||||
// --- Template Handling ---
|
||||
$tplModel = new VoucherTemplateModel();
|
||||
$templates = $tplModel->getAll(); // Need session? Model usually handles simple select, maybe filter by session later if needed? Schema says global?
|
||||
// Verification: Schema in implementation plan says id, name, content... doesn't mention session. Assuming global.
|
||||
|
||||
$currentTemplate = $_GET['template'] ?? 'default';
|
||||
$templateContent = '';
|
||||
|
||||
$viewName = 'print/default';
|
||||
|
||||
if ($currentTemplate !== 'default') {
|
||||
$tpl = $tplModel->getById($currentTemplate);
|
||||
if ($tpl) {
|
||||
$templateContent = $tpl['content'];
|
||||
$viewName = 'print/custom';
|
||||
} else {
|
||||
// Fallback if ID invalid
|
||||
$currentTemplate = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Logo Handling ---
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$logos = $logoModel->getAll();
|
||||
$logoMap = [];
|
||||
foreach ($logos as $l) {
|
||||
$logoMap[$l['id']] = $l['path'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'users' => $validUsers,
|
||||
'templates' => $templates,
|
||||
'currentTemplate' => $currentTemplate,
|
||||
'templateContent' => $templateContent,
|
||||
'session' => $session,
|
||||
'logoMap' => $logoMap
|
||||
];
|
||||
|
||||
return $this->view($viewName, $data);
|
||||
}
|
||||
}
|
||||
119
app/Controllers/InstallController.php
Normal file
119
app/Controllers/InstallController.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Core\Database;
|
||||
use App\Core\Migrations;
|
||||
use App\Config\SiteConfig;
|
||||
|
||||
class InstallController extends Controller {
|
||||
|
||||
public function index() {
|
||||
// Check if already installed
|
||||
if ($this->isInstalled()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
return $this->view('install');
|
||||
}
|
||||
|
||||
public function process() {
|
||||
if ($this->isInstalled()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_POST['username'] ?? 'admin';
|
||||
$password = $_POST['password'] ?? 'admin';
|
||||
|
||||
try {
|
||||
// 1. Run Migrations
|
||||
Migrations::up();
|
||||
|
||||
// 2. Generate Key if default
|
||||
if (SiteConfig::getSecretKey() === 'mikhmonv3remake_secret_key_32bytes') {
|
||||
$this->generateKey();
|
||||
}
|
||||
|
||||
// 3. Create Admin
|
||||
$db = Database::getInstance();
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// Check if user exists (edge case where key was default but user existed)
|
||||
$check = $db->query("SELECT id FROM users WHERE username = ?", [$username])->fetch();
|
||||
if (!$check) {
|
||||
$db->query("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)", [
|
||||
$username, $hash, date('Y-m-d H:i:s')
|
||||
]);
|
||||
} else {
|
||||
$db->query("UPDATE users SET password = ? WHERE username = ?", [$hash, $username]);
|
||||
}
|
||||
|
||||
// Success
|
||||
\App\Helpers\FlashHelper::set('success', 'Installation Complete', 'System has been successfully installed. Please login.');
|
||||
header('Location: /login');
|
||||
exit;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\App\Helpers\FlashHelper::set('error', 'Installation Failed', $e->getMessage());
|
||||
header('Location: /install');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private function isInstalled() {
|
||||
// Check if .env exists and APP_KEY is set to something other than the default/example
|
||||
$envPath = ROOT . '/.env';
|
||||
if (!file_exists($envPath)) {
|
||||
// Check if SiteConfig has a manual override (legacy)
|
||||
return SiteConfig::getSecretKey() !== 'mikhmonv3remake_secret_key_32bytes';
|
||||
}
|
||||
|
||||
$key = getenv('APP_KEY');
|
||||
$keyChanged = ($key && $key !== 'mikhmonv3remake_secret_key_32bytes');
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$count = $db->query("SELECT count(*) as c FROM users")->fetchColumn();
|
||||
$hasUser = ($count > 0);
|
||||
} catch (\Exception $e) {
|
||||
$hasUser = false;
|
||||
}
|
||||
|
||||
return $keyChanged && $hasUser;
|
||||
}
|
||||
|
||||
private function generateKey() {
|
||||
$key = bin2hex(random_bytes(16));
|
||||
$envPath = ROOT . '/.env';
|
||||
$examplePath = ROOT . '/.env.example';
|
||||
|
||||
if (!file_exists($envPath)) {
|
||||
if (file_exists($examplePath)) {
|
||||
copy($examplePath, $envPath);
|
||||
} else {
|
||||
return; // Cannot generate without source
|
||||
}
|
||||
}
|
||||
|
||||
$content = file_get_contents($envPath);
|
||||
|
||||
if (strpos($content, 'APP_KEY=') !== false) {
|
||||
$newContent = preg_replace(
|
||||
"/APP_KEY=.*/",
|
||||
"APP_KEY=$key",
|
||||
$content
|
||||
);
|
||||
} else {
|
||||
$newContent = $content . "\nAPP_KEY=$key";
|
||||
}
|
||||
|
||||
file_put_contents($envPath, $newContent);
|
||||
|
||||
// Refresh env in current session so next steps use it
|
||||
putenv("APP_KEY=$key");
|
||||
$_ENV['APP_KEY'] = $key;
|
||||
}
|
||||
}
|
||||
55
app/Controllers/LogController.php
Normal file
55
app/Controllers/LogController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
class LogController extends Controller
|
||||
{
|
||||
public function index($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
if (!$config) return header('Location: /');
|
||||
|
||||
$logs = [];
|
||||
$API = new RouterOSAPI();
|
||||
$API->attempts = 1;
|
||||
$API->timeout = 3;
|
||||
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
// Fetch Hotspot Logs
|
||||
// /log/print where topics~hotspot
|
||||
// In API we can't always filter effectively by topic in all versions,
|
||||
// but we can try ?topics=hotspot,info or similar.
|
||||
// Safe bet: fetch last 100 logs and filter PHP side or use API filter if possible.
|
||||
// Using a limit to avoid timeout.
|
||||
|
||||
// Getting generic logs for now, filtered by topic 'hotspot' if possible.
|
||||
// RouterOS API query for array search: ?topics=hotspot
|
||||
|
||||
$logs = $API->comm("/log/print", [
|
||||
"?topics" => "hotspot,info,debug", // Try detailed match
|
||||
]);
|
||||
|
||||
// Fallback if strict match fails, just get recent logs
|
||||
if (empty($logs) || isset($logs['!trap'])) {
|
||||
$logs = $API->comm("/log/print", []); // Get all (capped usually by buffer)
|
||||
}
|
||||
|
||||
// Reverse to show newest first
|
||||
if (is_array($logs)) {
|
||||
$logs = array_reverse($logs);
|
||||
}
|
||||
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
return $this->view('reports/user_log', [
|
||||
'session' => $session,
|
||||
'logs' => $logs
|
||||
]);
|
||||
}
|
||||
}
|
||||
348
app/Controllers/ProfileController.php
Normal file
348
app/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function index($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
|
||||
if (!$creds) {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
// Use default port 8728 if not specified
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
$profiles = $API->comm('/ip/hotspot/user/profile/print');
|
||||
$API->disconnect();
|
||||
|
||||
// Process profiles to add metadata from on-login script
|
||||
foreach ($profiles as &$profile) {
|
||||
$meta = \App\Helpers\HotspotHelper::parseProfileMetadata($profile['on-login'] ?? '');
|
||||
$profile['meta'] = $meta;
|
||||
$profile['meta']['expired_mode_formatted'] = \App\Helpers\HotspotHelper::formatExpiredMode($meta['expired_mode'] ?? '');
|
||||
}
|
||||
|
||||
$this->view('hotspot/profiles/index', [
|
||||
'session' => $session,
|
||||
'profiles' => $profiles,
|
||||
'title' => 'User Profiles'
|
||||
]);
|
||||
} else {
|
||||
$this->view('hotspot/profiles/index', [
|
||||
'session' => $session,
|
||||
'profiles' => [],
|
||||
'error' => 'Connection Failed to ' . $creds['ip'],
|
||||
'title' => 'User Profiles'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function add($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$pools = [];
|
||||
$queues = [];
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
$pools = $API->comm('/ip/pool/print');
|
||||
|
||||
// Fetch Queues (Simple & Tree)
|
||||
$simple = $API->comm('/queue/simple/print');
|
||||
$tree = $API->comm('/queue/tree/print');
|
||||
|
||||
// Extract just names for dropdown
|
||||
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();
|
||||
}
|
||||
|
||||
$this->view('hotspot/profiles/add', [
|
||||
'session' => $session,
|
||||
'pools' => $pools,
|
||||
'queues' => $queues
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$name = $_POST['name'] ?? '';
|
||||
$sharedUsers = $_POST['shared-users'] ?? '1';
|
||||
$rateLimit = $_POST['rate-limit'] ?? '';
|
||||
$addressPool = $_POST['address-pool'] ?? 'none';
|
||||
$parentQueue = $_POST['parent-queue'] ?? 'none';
|
||||
|
||||
// Metadata fields
|
||||
$expiredMode = $_POST['expired_mode'] ?? 'none';
|
||||
|
||||
// Validity Logic
|
||||
$val_d = $_POST['validity_d'] ?? '';
|
||||
$val_h = $_POST['validity_h'] ?? '';
|
||||
$val_m = $_POST['validity_m'] ?? '';
|
||||
$validity = '';
|
||||
if($val_d) $validity .= $val_d . 'd';
|
||||
if($val_h) $validity .= $val_h . 'h';
|
||||
if($val_m) $validity .= $val_m . 'm';
|
||||
|
||||
$price = $_POST['price'] ?? '';
|
||||
$sellingPrice = $_POST['selling_price'] ?? '';
|
||||
$lockUser = $_POST['lock_user'] ?? 'Disable';
|
||||
|
||||
// Construct on-login script
|
||||
// Construct on-login script
|
||||
$metaScript = sprintf(
|
||||
':put (",%s,%s,%s,%s,,%s,")',
|
||||
$expiredMode,
|
||||
$price,
|
||||
$validity,
|
||||
$sellingPrice,
|
||||
$lockUser
|
||||
);
|
||||
|
||||
// Logic Script (The "Enforcer") - Enforces Calendar Validity
|
||||
// Automates adding a scheduler to Disable user after "Validity" time passes from first login.
|
||||
// Update: Added Self-Cleaning logic (:do {} on-error={}) to ensure scheduler deletes itself
|
||||
// even if user was manually deleted from Winbox.
|
||||
$logicScript = "";
|
||||
if (!empty($validity)) {
|
||||
$logicScript = ' :local v "'.$validity.'"; :local u $user; :local c [/ip hotspot user get [find name=$u] comment]; :if ([:find $c "exp"] = -1) do={ /sys sch add name=$u interval=$v on-event=":do { /ip hotspot user set [find name=$u] disabled=yes } on-error={}; /sys sch remove [find name=$u]"; /ip hotspot user set [find name=$u] comment=("exp: " . $v . " " . $c); }';
|
||||
}
|
||||
|
||||
$onLogin = $metaScript . $logicScript;
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
$profileData = [
|
||||
'name' => $name,
|
||||
'shared-users' => $sharedUsers,
|
||||
'on-login' => $onLogin,
|
||||
'address-pool' => $addressPool,
|
||||
'parent-queue' => $parentQueue
|
||||
];
|
||||
|
||||
if ($parentQueue === 'none') {
|
||||
unset($profileData['parent-queue']); // Or handle appropriately if Mikrotik accepts 'none' or unset
|
||||
}
|
||||
|
||||
if (!empty($rateLimit)) {
|
||||
$profileData['rate-limit'] = $rateLimit;
|
||||
}
|
||||
|
||||
$API->comm("/ip/hotspot/user/profile/add", $profileData);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.profile_created', 'toasts.profile_created_desc', ['name' => $name], true);
|
||||
$this->redirect('/' . $session . '/hotspot/profiles');
|
||||
}
|
||||
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
if (empty($session) || empty($id)) {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
$API->comm("/ip/hotspot/user/profile/remove", [
|
||||
".id" => $id,
|
||||
]);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.profile_deleted', 'toasts.profile_deleted_desc', [], true);
|
||||
$this->redirect('/' . $session . '/hotspot/profiles');
|
||||
}
|
||||
|
||||
public function edit($session, $id)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$profile = null;
|
||||
$pools = [];
|
||||
$queues = [];
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
$pools = $API->comm('/ip/pool/print');
|
||||
|
||||
// Fetch Queues (Simple & Tree)
|
||||
$simple = $API->comm('/queue/simple/print');
|
||||
$tree = $API->comm('/queue/tree/print');
|
||||
|
||||
foreach ($simple as $q) {
|
||||
if(isset($q['name'])) $queues[] = $q['name'];
|
||||
}
|
||||
foreach ($tree as $q) {
|
||||
if(isset($q['name'])) $queues[] = $q['name'];
|
||||
}
|
||||
sort($queues);
|
||||
|
||||
$profiles = $API->comm('/ip/hotspot/user/profile/print', [
|
||||
"?.id" => $id
|
||||
]);
|
||||
|
||||
if (!empty($profiles)) {
|
||||
$profile = $profiles[0];
|
||||
// Parse metadata
|
||||
$meta = \App\Helpers\HotspotHelper::parseProfileMetadata($profile['on-login'] ?? '');
|
||||
$profile['meta'] = $meta;
|
||||
|
||||
// Parse Validity
|
||||
$val_d = '';
|
||||
$val_h = '';
|
||||
$val_m = '';
|
||||
|
||||
if (!empty($meta['validity'])) {
|
||||
if (preg_match('/(\d+)d/', $meta['validity'], $m)) $val_d = $m[1];
|
||||
if (preg_match('/(\d+)h/', $meta['validity'], $m)) $val_h = $m[1];
|
||||
if (preg_match('/(\d+)m/', $meta['validity'], $m)) $val_m = $m[1];
|
||||
}
|
||||
|
||||
$profile['val_d'] = $val_d;
|
||||
$profile['val_h'] = $val_h;
|
||||
$profile['val_m'] = $val_m;
|
||||
}
|
||||
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
if (!$profile) {
|
||||
$this->redirect('/' . $session . '/hotspot/profiles');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->view('hotspot/profiles/edit', [
|
||||
'session' => $session,
|
||||
'profile' => $profile,
|
||||
'pools' => $pools,
|
||||
'queues' => $queues
|
||||
]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
$name = $_POST['name'] ?? '';
|
||||
$sharedUsers = $_POST['shared-users'] ?? '1';
|
||||
$rateLimit = $_POST['rate-limit'] ?? '';
|
||||
$addressPool = $_POST['address-pool'] ?? 'none';
|
||||
$parentQueue = $_POST['parent-queue'] ?? 'none';
|
||||
|
||||
// Metadata fields
|
||||
$expiredMode = $_POST['expired_mode'] ?? 'none';
|
||||
|
||||
// Validity Logic
|
||||
$val_d = $_POST['validity_d'] ?? '';
|
||||
$val_h = $_POST['validity_h'] ?? '';
|
||||
$val_m = $_POST['validity_m'] ?? '';
|
||||
$validity = '';
|
||||
if($val_d) $validity .= $val_d . 'd';
|
||||
if($val_h) $validity .= $val_h . 'h';
|
||||
if($val_m) $validity .= $val_m . 'm';
|
||||
|
||||
$price = $_POST['price'] ?? '';
|
||||
$sellingPrice = $_POST['selling_price'] ?? '';
|
||||
$lockUser = $_POST['lock_user'] ?? 'Disable';
|
||||
|
||||
$metaScript = sprintf(
|
||||
':put (",%s,%s,%s,%s,,%s,")',
|
||||
$expiredMode,
|
||||
$price,
|
||||
$validity,
|
||||
$sellingPrice,
|
||||
$lockUser
|
||||
);
|
||||
|
||||
// Logic Script (The "Enforcer")
|
||||
$logicScript = "";
|
||||
if (!empty($validity)) {
|
||||
$logicScript = ' :local v "'.$validity.'"; :local u $user; :local c [/ip hotspot user get [find name=$u] comment]; :if ([:find $c "exp"] = -1) do={ /sys sch add name=$u interval=$v on-event=":do { /ip hotspot user set [find name=$u] disabled=yes } on-error={}; /sys sch remove [find name=$u]"; /ip hotspot user set [find name=$u] comment=("exp: " . $v . " " . $c); }';
|
||||
}
|
||||
|
||||
$onLogin = $metaScript . $logicScript;
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) return;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) {
|
||||
$profileData = [
|
||||
'.id' => $id,
|
||||
'name' => $name,
|
||||
'shared-users' => $sharedUsers,
|
||||
'on-login' => $onLogin,
|
||||
'address-pool' => $addressPool,
|
||||
'parent-queue' => $parentQueue
|
||||
];
|
||||
|
||||
if ($parentQueue === 'none') {
|
||||
unset($profileData['parent-queue']);
|
||||
}
|
||||
|
||||
$profileData['rate-limit'] = $rateLimit;
|
||||
|
||||
$API->comm("/ip/hotspot/user/profile/set", $profileData);
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.profile_updated', 'toasts.profile_updated_desc', ['name' => $name], true);
|
||||
$this->redirect('/' . $session . '/hotspot/profiles');
|
||||
}
|
||||
}
|
||||
240
app/Controllers/PublicStatusController.php
Normal file
240
app/Controllers/PublicStatusController.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Config\SiteConfig;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
use App\Helpers\EncryptionHelper;
|
||||
use App\Helpers\FormatHelper;
|
||||
|
||||
class PublicStatusController extends Controller {
|
||||
|
||||
// View: Show Search Page
|
||||
public function index($session) {
|
||||
// Just verify session existence to display Hotspot Name
|
||||
$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,
|
||||
'hotspot_name' => $creds['hotspot_name'] ?? 'Hotspot',
|
||||
'footer_text' => SiteConfig::getFooter()
|
||||
];
|
||||
|
||||
return $this->view('public/status', $data);
|
||||
}
|
||||
|
||||
// API: Check Status
|
||||
public function check($codeUrl = null) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Allow POST and GET
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method Not Allowed']);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
// Session: Try Body -> Try Header
|
||||
$session = $input['session'] ?? '';
|
||||
if (empty($session)) {
|
||||
$headers = getallheaders();
|
||||
// Handle case-insensitivity of headers
|
||||
$session = $headers['X-Mivo-Session'] ?? ($headers['x-mivo-session'] ?? '');
|
||||
}
|
||||
|
||||
// Code: Can be in URL or Body
|
||||
$code = $codeUrl ?? ($input['code'] ?? '');
|
||||
|
||||
if (empty($session) || empty($code)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Session and Voucher Code are required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
|
||||
if (!$creds) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password = RouterOSAPI::decrypt($password);
|
||||
}
|
||||
|
||||
$api = new RouterOSAPI();
|
||||
if (!$api->connect($creds['ip'], $creds['user'], $password)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Router Connection Failed']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Logic Refactor: Pivot to User Table as primary source for Voucher Details
|
||||
// 1. Check User in Database
|
||||
$user = $api->comm("/ip/hotspot/user/print", [
|
||||
"?name" => $code
|
||||
]);
|
||||
|
||||
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);
|
||||
$bytesOut = intval($u['bytes-out'] ?? 0);
|
||||
|
||||
if (($uptimeRaw === '0s' || empty($uptimeRaw)) && ($bytesIn + $bytesOut) === 0) {
|
||||
$api->disconnect();
|
||||
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SECURITY CHECK: Hide Unlimited Members ---
|
||||
$limitBytes = isset($u['limit-bytes-total']) ? intval($u['limit-bytes-total']) : 0;
|
||||
$limitUptime = $u['limit-uptime'] ?? '0s';
|
||||
|
||||
if ($limitBytes === 0 && ($limitUptime === '0s' || empty($limitUptime))) {
|
||||
// Option: Allow checking them but show minimalistic info, or hide.
|
||||
// Sticking to original logic: Hide them.
|
||||
$api->disconnect();
|
||||
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- CALCULATIONS ---
|
||||
$dataUsed = $bytesIn + $bytesOut;
|
||||
$dataLeft = 'Unlimited';
|
||||
|
||||
if ($limitBytes > 0) {
|
||||
$remaining = max(0, $limitBytes - $dataUsed);
|
||||
$dataLeft = ($remaining === 0) ? '0 B' : FormatHelper::formatBytes($remaining);
|
||||
}
|
||||
|
||||
// Validity Logic
|
||||
$validityRaw = $u['limit-uptime'] ?? '0s';
|
||||
$validityDisplay = ($validityRaw === '0s') ? 'Unlimited' : FormatHelper::elapsedTime($validityRaw);
|
||||
$expiration = '-';
|
||||
|
||||
$comment = strtolower($u['comment'] ?? '');
|
||||
if (preg_match('/exp\W+([a-z]{3}\/\d{2}\/\d{4}\s\d{2}:\d{2}:\d{2})/', $comment, $matches)) {
|
||||
$expiration = $matches[1];
|
||||
} elseif ($validityRaw !== '0s') {
|
||||
$totalSeconds = FormatHelper::parseDuration($validityRaw);
|
||||
$usedSeconds = FormatHelper::parseDuration($uptimeRaw);
|
||||
$remainingSeconds = max(0, $totalSeconds - $usedSeconds);
|
||||
|
||||
if ($remainingSeconds > 0) {
|
||||
$expiration = date('d M Y H:i', time() + $remainingSeconds);
|
||||
} else {
|
||||
$expiration = 'Expired';
|
||||
}
|
||||
}
|
||||
|
||||
// BASE STATUS
|
||||
$status = 'offline';
|
||||
$statusLabel = 'Valid / Offline';
|
||||
$isDisabled = ($u['disabled'] ?? 'false') === 'true';
|
||||
|
||||
// Calculate Time Left
|
||||
$timeLeft = 'Unlimited';
|
||||
if ($expiration !== '-' && $expiration !== 'Expired') {
|
||||
$expTime = strtotime($expiration);
|
||||
if ($expTime) {
|
||||
$rem = max(0, $expTime - time());
|
||||
$timeLeft = ($rem === 0) ? 'Expired' : FormatHelper::formatSeconds($rem);
|
||||
}
|
||||
} elseif ($validityRaw !== '0s') {
|
||||
$totalSeconds = FormatHelper::parseDuration($validityRaw);
|
||||
$usedSeconds = FormatHelper::parseDuration($uptimeRaw);
|
||||
$rem = max(0, $totalSeconds - $usedSeconds);
|
||||
$timeLeft = ($rem === 0) ? 'Expired' : FormatHelper::formatSeconds($rem);
|
||||
}
|
||||
|
||||
if (strpos($comment, 'exp') !== false || ($expiration === 'Expired')) {
|
||||
$status = 'expired';
|
||||
$statusLabel = 'Expired';
|
||||
} elseif ($limitBytes > 0 && $dataUsed >= $limitBytes) {
|
||||
$status = 'limited';
|
||||
$statusLabel = 'Quota Exceeded';
|
||||
} elseif ($isDisabled) {
|
||||
$status = 'locked';
|
||||
$statusLabel = 'Locked / Disabled';
|
||||
}
|
||||
|
||||
// 2. CHECK ACTIVE OVERRIDE
|
||||
// If user is conceptually valid (or even if limited?), check if they are currently active
|
||||
// Because they might be active BUT expiring soon, or active BUT over quota (if server hasn't kicked them yet)
|
||||
$active = $api->comm("/ip/hotspot/active/print", [
|
||||
"?user" => $code
|
||||
]);
|
||||
|
||||
if (!empty($active)) {
|
||||
$status = 'active';
|
||||
$statusLabel = 'Active (Online)';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'status' => $status,
|
||||
'status_label' => $statusLabel,
|
||||
'username' => $u['name'] ?? 'Unknown',
|
||||
'profile' => $u['profile'] ?? 'default',
|
||||
'uptime_used' => FormatHelper::elapsedTime($uptimeRaw),
|
||||
'validity' => $validityDisplay,
|
||||
'data_used' => FormatHelper::formatBytes($dataUsed),
|
||||
'data_left' => $dataLeft,
|
||||
'expiration' => $expiration,
|
||||
'time_left' => $timeLeft,
|
||||
'comment' => $u['comment'] ?? '',
|
||||
];
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $data]);
|
||||
$api->disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Fallback: Check Active Only (Trial Users or IP Bindings not in User Table)
|
||||
$active = $api->comm("/ip/hotspot/active/print", [
|
||||
"?user" => $code
|
||||
]);
|
||||
|
||||
if (!empty($active)) {
|
||||
$u = $active[0];
|
||||
$data = [
|
||||
'status' => 'active',
|
||||
'status_label' => 'Active (Online)',
|
||||
'username' => $u['user'] ?? 'Unknown',
|
||||
'profile' => '-', // Active usually doesn't have profile name directly unless queried
|
||||
'uptime_used' => FormatHelper::elapsedTime($u['uptime'] ?? '0s'),
|
||||
'validity' => '-',
|
||||
'data_used' => FormatHelper::formatBytes(intval($u['bytes-in'] ?? 0) + intval($u['bytes-out'] ?? 0)),
|
||||
'data_left' => 'Unknown',
|
||||
'time_left' => isset($u['session-time-left']) ? FormatHelper::elapsedTime($u['session-time-left']) : '-',
|
||||
'expiration' => '-',
|
||||
'comment' => ''
|
||||
];
|
||||
echo json_encode(['success' => true, 'data' => $data]);
|
||||
$api->disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
$api->disconnect();
|
||||
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
|
||||
}
|
||||
}
|
||||
220
app/Controllers/QuickPrintController.php
Normal file
220
app/Controllers/QuickPrintController.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Models\QuickPrintModel;
|
||||
use App\Models\VoucherTemplateModel;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
use App\Core\Middleware;
|
||||
use App\Helpers\EncryptionHelper;
|
||||
|
||||
class QuickPrintController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
Middleware::auth();
|
||||
}
|
||||
|
||||
// Dashboard: List Cards
|
||||
public function index($session) {
|
||||
$qpModel = new QuickPrintModel();
|
||||
$packages = $qpModel->getAllBySession($session);
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'packages' => $packages
|
||||
];
|
||||
// Note: View will be 'quick_print/index'
|
||||
return $this->view('quick_print/index', $data);
|
||||
}
|
||||
|
||||
// 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);
|
||||
$profiles = [];
|
||||
if ($creds) {
|
||||
$API = new RouterOSAPI();
|
||||
$password = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password = RouterOSAPI::decrypt($password);
|
||||
}
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password)) {
|
||||
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'packages' => $packages,
|
||||
'profiles' => $profiles
|
||||
];
|
||||
return $this->view('quick_print/list', $data);
|
||||
}
|
||||
|
||||
// CRUD: Store
|
||||
public function store() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
|
||||
$session = $_POST['session'] ?? '';
|
||||
$data = [
|
||||
'session_name' => $session,
|
||||
'name' => $_POST['name'] ?? 'Package',
|
||||
'server' => $_POST['server'] ?? 'all',
|
||||
'profile' => $_POST['profile'] ?? 'default',
|
||||
'prefix' => $_POST['prefix'] ?? '',
|
||||
'char_length' => $_POST['char_length'] ?? 4,
|
||||
'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->add($data);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.package_saved', 'toasts.package_saved_desc', [], true);
|
||||
header("Location: /" . $session . "/quick-print/manage");
|
||||
exit;
|
||||
}
|
||||
|
||||
// CRUD: Delete
|
||||
public function delete() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
$session = $_POST['session'] ?? '';
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
$qpModel = new QuickPrintModel();
|
||||
$qpModel->delete($id);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.package_deleted', 'toasts.package_deleted_desc', [], true);
|
||||
header("Location: /" . $session . "/quick-print/manage");
|
||||
exit;
|
||||
}
|
||||
|
||||
// ACTION: Generate User & Print
|
||||
public function printPacket($session, $id) {
|
||||
// 1. Get Package Details
|
||||
$qpModel = new QuickPrintModel();
|
||||
$package = $qpModel->getById($id);
|
||||
|
||||
if (!$package) {
|
||||
die("Package not found");
|
||||
}
|
||||
|
||||
// 2. Generate Credentials
|
||||
$prefix = $package['prefix'];
|
||||
$length = $package['char_length'];
|
||||
$charSet = '1234567890abcdefghijklmnopqrstuvwxyz'; // Simple lowercase + num
|
||||
$rand = substr(str_shuffle($charSet), 0, $length);
|
||||
$username = $prefix . $rand;
|
||||
$password = $username; // Default: user=pass (User Mode) - Can be improved later
|
||||
|
||||
// 3. Connect to Mikrotik & Add User
|
||||
$configModel = new Config();
|
||||
$creds = $configModel->getSession($session);
|
||||
if (!$creds) die("Session error");
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$password_router = $creds['password'];
|
||||
if (isset($creds['source']) && $creds['source'] === 'legacy') {
|
||||
$password_router = RouterOSAPI::decrypt($password_router);
|
||||
}
|
||||
|
||||
if ($API->connect($creds['ip'], $creds['user'], $password_router)) {
|
||||
$userData = [
|
||||
'name' => $username,
|
||||
'password' => $password,
|
||||
'profile' => $package['profile'],
|
||||
'comment' => $package['comment'] . " [QP]" // Mark as QuickPrint
|
||||
];
|
||||
|
||||
// Limits
|
||||
if(!empty($package['time_limit'])) $userData['limit-uptime'] = $package['time_limit'];
|
||||
if(!empty($package['data_limit'])) {
|
||||
// Check if M or G
|
||||
// Simple logic for now, assuming raw if number, or passing string if Mikrotik accepts it (usually requires bytes)
|
||||
// Let's assume user inputs "100M" or "1G" which usually needs parsing.
|
||||
// For now, let's assume input is NUMBER in MB as per standard Mikhmon practice, OR generic string.
|
||||
// We'll pass as is for strings, or multiply if strictly numeric?
|
||||
// Let's rely on standard Mikrotik parsing if string passed, or convert.
|
||||
// Mikhmon v3 usually uses dropdown "MB/GB".
|
||||
// Implementing simple conversion:
|
||||
$val = intval($package['data_limit']);
|
||||
if (strpos(strtolower($package['data_limit']), 'g') !== false) {
|
||||
$userData['limit-bytes-total'] = $val * 1024 * 1024 * 1024;
|
||||
} else {
|
||||
$userData['limit-bytes-total'] = $val * 1024 * 1024; // Default MB
|
||||
}
|
||||
}
|
||||
|
||||
$API->comm("/ip/hotspot/user/add", $userData);
|
||||
$API->disconnect();
|
||||
} else {
|
||||
die("Connection failed");
|
||||
}
|
||||
|
||||
|
||||
// 4. Render Template
|
||||
$tplModel = new VoucherTemplateModel();
|
||||
$templates = $tplModel->getAll();
|
||||
|
||||
$currentTemplate = $_GET['template'] ?? 'default';
|
||||
$templateContent = '';
|
||||
$viewName = 'print/default';
|
||||
|
||||
if ($currentTemplate !== 'default') {
|
||||
$tpl = $tplModel->getById($currentTemplate);
|
||||
if ($tpl) {
|
||||
$templateContent = $tpl['content'];
|
||||
$viewName = 'print/custom';
|
||||
} else {
|
||||
$currentTemplate = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate bytes for display
|
||||
$dlVal = intval($package['data_limit']);
|
||||
$bytes = (strpos(strtolower($package['data_limit']), 'g') !== false) ? $dlVal * 1024*1024*1024 : $dlVal * 1024*1024;
|
||||
|
||||
$userDataValues = [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'price' => $package['price'],
|
||||
'validity' => $package['time_limit'],
|
||||
'timelimit' => \App\Helpers\HotspotHelper::formatValidity($package['time_limit']),
|
||||
'datalimit' => \App\Helpers\HotspotHelper::formatBytes($bytes),
|
||||
'profile' => $package['profile'],
|
||||
'comment' => 'Quick Print',
|
||||
'hotspotname' => $creds['hotspot_name'],
|
||||
'dns_name' => $creds['dns_name'],
|
||||
'login_url' => (preg_match("~^(?:f|ht)tps?://~i", $creds['dns_name']) ? $creds['dns_name'] : "http://" . $creds['dns_name']) . "/login"
|
||||
];
|
||||
|
||||
// --- Logo Handling ---
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$logos = $logoModel->getAll();
|
||||
$logoMap = [];
|
||||
foreach ($logos as $l) {
|
||||
$logoMap[$l['id']] = $l['path'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'users' => [$userDataValues],
|
||||
'templates' => $templates,
|
||||
'currentTemplate' => $currentTemplate,
|
||||
'templateContent' => $templateContent,
|
||||
'session' => $session,
|
||||
'logoMap' => $logoMap
|
||||
];
|
||||
|
||||
return $this->view($viewName, $data);
|
||||
}
|
||||
}
|
||||
165
app/Controllers/ReportController.php
Normal file
165
app/Controllers/ReportController.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
use App\Helpers\HotspotHelper;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function index($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$users = [];
|
||||
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
// Fetch All Users
|
||||
// Optimized print: get .id, name, price, comment
|
||||
$users = $API->comm("/ip/hotspot/user/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
// Aggregate Data
|
||||
$report = [];
|
||||
$totalIncome = 0;
|
||||
$totalVouchers = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Skip if no price
|
||||
if (empty($user['price']) || $user['price'] == '0') continue;
|
||||
|
||||
// Determine Date from Comment
|
||||
// Mikhmon format usually: "mikhmon-MM/DD/YYYY" or just "MM/DD/YYYY" or plain comment
|
||||
// We will try to parse a date from the comment, or use "Unknown Date"
|
||||
$date = 'Unknown Date';
|
||||
$comment = $user['comment'] ?? '';
|
||||
|
||||
// Regex for date patterns (d-m-Y or m/d/Y or Y-m-d)
|
||||
// Simplify: Group by Comment content itself if it looks like a date/batch
|
||||
// Or try to extract M-Y.
|
||||
|
||||
// For feature parity, Mikhmon often groups by the exact comment string as the "Batch/Date"
|
||||
if (!empty($comment)) {
|
||||
$date = $comment;
|
||||
}
|
||||
|
||||
if (!isset($report[$date])) {
|
||||
$report[$date] = [
|
||||
'date' => $date,
|
||||
'count' => 0,
|
||||
'total' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$price = intval($user['price']);
|
||||
$report[$date]['count']++;
|
||||
$report[$date]['total'] += $price;
|
||||
|
||||
$totalIncome += $price;
|
||||
$totalVouchers++;
|
||||
}
|
||||
|
||||
// Sort by key (Date/Comment) desc
|
||||
krsort($report);
|
||||
|
||||
return $this->view('reports/selling', [
|
||||
'session' => $session,
|
||||
'report' => $report,
|
||||
'totalIncome' => $totalIncome,
|
||||
'totalVouchers' => $totalVouchers,
|
||||
'currency' => $config['currency'] ?? 'Rp'
|
||||
]);
|
||||
}
|
||||
public function resume($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$users = [];
|
||||
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$users = $API->comm("/ip/hotspot/user/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
// Initialize Aggregates
|
||||
$daily = [];
|
||||
$monthly = [];
|
||||
$yearly = [];
|
||||
$totalIncome = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (empty($user['price']) || $user['price'] == '0') continue;
|
||||
|
||||
// Try to parse Date from Comment (Mikhmon format: mikhmon-10/25/2023 or just 10/25/2023)
|
||||
$comment = $user['comment'] ?? '';
|
||||
$dateObj = null;
|
||||
|
||||
// Simple parser: try to find MM/DD/YYYY
|
||||
if (preg_match('/(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{2,4})/', $comment, $matches)) {
|
||||
// Assuming MM/DD/YYYY based on typical Mikhmon, but could be DD-MM-YYYY
|
||||
// Let's standardise on checking valid date.
|
||||
// Standard Mikhmon V3 is MM/DD/YYYY.
|
||||
$m = $matches[1];
|
||||
$d = $matches[2];
|
||||
$y = $matches[3];
|
||||
if (strlen($y) == 2) $y = '20' . $y;
|
||||
$dateObj = \DateTime::createFromFormat('m/d/Y', "$m/$d/$y");
|
||||
}
|
||||
|
||||
// Fallback: If no date found in comment, maybe created at?
|
||||
// Usually Mikhmon relies strictly on comment.
|
||||
if (!$dateObj) continue;
|
||||
|
||||
$price = intval($user['price']);
|
||||
$totalIncome += $price;
|
||||
|
||||
// Formats
|
||||
$dayKey = $dateObj->format('Y-m-d');
|
||||
$monthKey = $dateObj->format('Y-m');
|
||||
$yearKey = $dateObj->format('Y');
|
||||
|
||||
// Daily
|
||||
if (!isset($daily[$dayKey])) $daily[$dayKey] = 0;
|
||||
$daily[$dayKey] += $price;
|
||||
|
||||
// Monthly
|
||||
if (!isset($monthly[$monthKey])) $monthly[$monthKey] = 0;
|
||||
$monthly[$monthKey] += $price;
|
||||
|
||||
// Yearly
|
||||
if (!isset($yearly[$yearKey])) $yearly[$yearKey] = 0;
|
||||
$yearly[$yearKey] += $price;
|
||||
}
|
||||
|
||||
// Sort Keys
|
||||
ksort($daily);
|
||||
ksort($monthly);
|
||||
ksort($yearly);
|
||||
|
||||
return $this->view('reports/resume', [
|
||||
'session' => $session,
|
||||
'daily' => $daily,
|
||||
'monthly' => $monthly,
|
||||
'yearly' => $yearly,
|
||||
'totalIncome' => $totalIncome,
|
||||
'currency' => $config['currency'] ?? 'Rp'
|
||||
]);
|
||||
}
|
||||
}
|
||||
97
app/Controllers/SchedulerController.php
Normal file
97
app/Controllers/SchedulerController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
class SchedulerController extends Controller
|
||||
{
|
||||
public function index($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$schedulers = [];
|
||||
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$schedulers = $API->comm("/system/scheduler/print");
|
||||
$API->disconnect();
|
||||
}
|
||||
|
||||
return $this->view('system/scheduler', [
|
||||
'session' => $session,
|
||||
'schedulers' => $schedulers
|
||||
]);
|
||||
}
|
||||
|
||||
public function store($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
if (!$config) exit;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$API->comm("/system/scheduler/add", [
|
||||
"name" => $_POST['name'],
|
||||
"on-event" => $_POST['on_event'],
|
||||
"start-date" => $_POST['start_date'],
|
||||
"start-time" => $_POST['start_time'],
|
||||
"interval" => $_POST['interval'],
|
||||
"comment" => $_POST['comment'] ?? '',
|
||||
"disabled" => "no"
|
||||
]);
|
||||
$API->disconnect();
|
||||
}
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.schedule_added', 'toasts.schedule_added_desc', [], true);
|
||||
header("Location: /$session/system/scheduler");
|
||||
}
|
||||
|
||||
public function update($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
if (!$config) exit;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$API->comm("/system/scheduler/set", [
|
||||
".id" => $_POST['id'],
|
||||
"name" => $_POST['name'],
|
||||
"on-event" => $_POST['on_event'],
|
||||
"start-date" => $_POST['start_date'],
|
||||
"start-time" => $_POST['start_time'],
|
||||
"interval" => $_POST['interval'],
|
||||
"comment" => $_POST['comment'] ?? ''
|
||||
]);
|
||||
$API->disconnect();
|
||||
}
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.schedule_updated', 'toasts.schedule_updated_desc', [], true);
|
||||
header("Location: /$session/system/scheduler");
|
||||
}
|
||||
|
||||
public function delete($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
if (!$config) exit;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$API->comm("/system/scheduler/remove", [
|
||||
".id" => $_POST['id']
|
||||
]);
|
||||
$API->disconnect();
|
||||
}
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.schedule_deleted', 'toasts.schedule_deleted_desc', [], true);
|
||||
header("Location: /$session/system/scheduler");
|
||||
}
|
||||
}
|
||||
462
app/Controllers/SettingsController.php
Normal file
462
app/Controllers/SettingsController.php
Normal file
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Core\Middleware;
|
||||
use App\Helpers\FormatHelper;
|
||||
|
||||
class SettingsController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
Middleware::auth();
|
||||
}
|
||||
|
||||
public function system() {
|
||||
// Systems Settings Tab (Admin, Global, Backup)
|
||||
$settingModel = new \App\Models\Setting();
|
||||
$settings = $settingModel->getAll();
|
||||
|
||||
$username = $_SESSION['username'] ?? 'admin';
|
||||
|
||||
return $this->view('settings/systems', [
|
||||
'settings' => $settings,
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
public function routers() {
|
||||
// Routers List Tab
|
||||
$configModel = new Config();
|
||||
$routers = $configModel->getAllSessions();
|
||||
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)
|
||||
$rawSess = $_POST['sessname'] ?? '';
|
||||
$sessName = preg_replace('/[^a-z0-9-]/', '', strtolower(str_replace(' ', '-', $rawSess)));
|
||||
|
||||
$data = [
|
||||
'session_name' => $sessName,
|
||||
'ip_address' => $_POST['ipmik'],
|
||||
'username' => $_POST['usermik'],
|
||||
'password' => $_POST['passmik'],
|
||||
'hotspot_name' => $_POST['hotspotname'],
|
||||
'dns_name' => $_POST['dnsname'],
|
||||
'currency' => $_POST['currency'],
|
||||
'reload_interval' => $_POST['areload'],
|
||||
'interface' => $_POST['iface'],
|
||||
'description' => 'Added via Remake',
|
||||
'quick_access' => isset($_POST['quick_access']) ? 1 : 0
|
||||
];
|
||||
|
||||
$configModel = new Config();
|
||||
try {
|
||||
$configModel->addSession($data);
|
||||
|
||||
$redirect = '/settings/routers';
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'connect') {
|
||||
$redirect = '/' . urlencode($data['session_name']) . '/dashboard';
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.router_added', 'toasts.router_added_desc', ['name' => $data['session_name']], true);
|
||||
header("Location: $redirect");
|
||||
} catch (\Exception $e) {
|
||||
echo "Error adding session: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Update Admin Password
|
||||
public function updateAdmin() {
|
||||
$newPassword = $_POST['admin_password'] ?? '';
|
||||
|
||||
if (!empty($newPassword)) {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$hash = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
// Assuming we are updating the default 'admin' user or the currently logged in user
|
||||
// Original Mikhmon usually has one main user. Let's update 'admin' for now.
|
||||
$db->query("UPDATE users SET password = ? WHERE username = 'admin'", [$hash]);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.password_updated', 'toasts.password_updated_desc', [], true);
|
||||
}
|
||||
|
||||
header('Location: /settings/system');
|
||||
}
|
||||
|
||||
// Update Global Settings
|
||||
public function updateGlobal() {
|
||||
$settingModel = new \App\Models\Setting();
|
||||
|
||||
if (isset($_POST['quick_print_mode'])) {
|
||||
$settingModel->set('quick_print_mode', $_POST['quick_print_mode']);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.settings_saved', 'toasts.settings_saved_desc', [], true);
|
||||
}
|
||||
|
||||
header('Location: /settings/system');
|
||||
}
|
||||
|
||||
|
||||
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'];
|
||||
|
||||
// Sanitize Session Name
|
||||
$rawSess = $_POST['sessname'] ?? '';
|
||||
$sessName = preg_replace('/[^a-z0-9-]/', '', strtolower(str_replace(' ', '-', $rawSess)));
|
||||
|
||||
$data = [
|
||||
'session_name' => $sessName,
|
||||
'ip_address' => $_POST['ipmik'],
|
||||
'username' => $_POST['usermik'],
|
||||
'password' => $_POST['passmik'], // Can be empty if not changing
|
||||
'hotspot_name' => $_POST['hotspotname'],
|
||||
'dns_name' => $_POST['dnsname'],
|
||||
'currency' => $_POST['currency'],
|
||||
'reload_interval' => $_POST['areload'],
|
||||
'interface' => $_POST['iface'],
|
||||
'description' => 'Updated via Remake',
|
||||
'quick_access' => isset($_POST['quick_access']) ? 1 : 0
|
||||
];
|
||||
|
||||
$configModel = new Config();
|
||||
try {
|
||||
$configModel->updateSession($id, $data);
|
||||
|
||||
$redirect = '/settings/routers';
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'connect') {
|
||||
$redirect = '/' . urlencode($data['session_name']) . '/dashboard';
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.router_updated', 'toasts.router_updated_desc', ['name' => $data['session_name']], true);
|
||||
header("Location: $redirect");
|
||||
} catch (\Exception $e) {
|
||||
echo "Error updating session: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$id = $_POST['id'];
|
||||
$configModel = new Config();
|
||||
$configModel->deleteSession($id);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.router_deleted', 'toasts.router_deleted_desc', [], true);
|
||||
header('Location: /settings/routers');
|
||||
}
|
||||
|
||||
public function backup() {
|
||||
$backupName = 'mivo_backup_' . date('d-m-Y') . '.mivo';
|
||||
$json = [];
|
||||
|
||||
// Backup Settings
|
||||
$settingModel = new \App\Models\Setting();
|
||||
$settings = $settingModel->getAll();
|
||||
$json['settings'] = $settings;
|
||||
|
||||
// Backup Sessions
|
||||
$configModel = new Config();
|
||||
$sessions = $configModel->getAllSessions();
|
||||
|
||||
// Decrypt passwords for portability
|
||||
foreach ($sessions as &$session) {
|
||||
if (!empty($session['password'])) {
|
||||
$session['password'] = \App\Helpers\EncryptionHelper::decrypt($session['password']);
|
||||
}
|
||||
}
|
||||
$json['sessions'] = $sessions;
|
||||
|
||||
// Backup Voucher Templates
|
||||
$templateModel = new \App\Models\VoucherTemplateModel();
|
||||
$json['voucher_templates'] = $templateModel->getAll();
|
||||
|
||||
// Backup Logos
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$logos = $logoModel->getAll();
|
||||
foreach ($logos as &$logo) {
|
||||
$filePath = ROOT . '/public' . $logo['path'];
|
||||
if (file_exists($filePath)) {
|
||||
$logo['data'] = base64_encode(file_get_contents($filePath));
|
||||
}
|
||||
}
|
||||
$json['logos'] = $logos;
|
||||
|
||||
// Encode
|
||||
$jsonString = json_encode($json, JSON_PRETTY_PRINT);
|
||||
|
||||
// Encrypt the entire file content for security
|
||||
// Decrypted data inside (like passwords) remain plaintext relative to the JSON structure
|
||||
// ensuring portability if decrypted successfully.
|
||||
$content = \App\Helpers\EncryptionHelper::encrypt($jsonString);
|
||||
|
||||
// Force Download
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename='.basename($backupName));
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . strlen($content));
|
||||
ob_clean();
|
||||
flush();
|
||||
echo $content;
|
||||
exit;
|
||||
}
|
||||
|
||||
public function restore() {
|
||||
if (!isset($_FILES['backup_file']) || $_FILES['backup_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.no_file_selected', [], true);
|
||||
header('Location: /settings/system');
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['backup_file'];
|
||||
$filename = $file['name'];
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$mime = $file['type'];
|
||||
|
||||
// Validate Extension & MIME
|
||||
$allowedExtensions = ['mivo'];
|
||||
$allowedMimes = ['application/octet-stream', 'text/plain']; // text/plain fallback for some OS/Browsers
|
||||
|
||||
if (!in_array($extension, $allowedExtensions) || (!empty($mime) && !in_array($mime, $allowedMimes))) {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.invalid_file_type_mivo', [], true);
|
||||
header('Location: /settings/system');
|
||||
exit;
|
||||
}
|
||||
|
||||
$rawValue = file_get_contents($file['tmp_name']);
|
||||
if (empty($rawValue)) {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.file_empty', [], true);
|
||||
header('Location: /settings/system');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Attempt to decrypt. If file is old (JSON plaintext), decrypt() returns it as-is.
|
||||
$content = \App\Helpers\EncryptionHelper::decrypt($rawValue);
|
||||
|
||||
$json = json_decode($content, true);
|
||||
|
||||
if (!$json || (!isset($json['settings']) && !isset($json['sessions']))) {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.file_corrupted', [], true);
|
||||
header('Location: /settings/system');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Restore Settings
|
||||
if (isset($json['settings'])) {
|
||||
$settingModel = new \App\Models\Setting();
|
||||
// Assuming we check if data exists
|
||||
// We might need to iterate and update
|
||||
foreach ($json['settings'] as $key => $val) {
|
||||
$settingModel->set($key, $val);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Sessions
|
||||
if (isset($json['sessions'])) {
|
||||
$configModel = new Config();
|
||||
foreach ($json['sessions'] as $session) {
|
||||
unset($session['id']); // Let system generate new ID
|
||||
try {
|
||||
$configModel->addSession($session);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Failed to restore session: " . ($session['session_name'] ?? 'unknown'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Voucher Templates
|
||||
if (isset($json['voucher_templates'])) {
|
||||
$templateModel = new \App\Models\VoucherTemplateModel();
|
||||
foreach ($json['voucher_templates'] as $tmpl) {
|
||||
// Check if template exists by name and session
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$existing = $db->query("SELECT id FROM voucher_templates WHERE name = ? AND session_name = ?", [$tmpl['name'], $tmpl['session_name']])->fetch();
|
||||
|
||||
if ($existing) {
|
||||
$templateModel->update($existing['id'], $tmpl);
|
||||
} else {
|
||||
$templateModel->add($tmpl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Logos
|
||||
if (isset($json['logos'])) {
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$uploadDir = ROOT . '/public/assets/img/logos/';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
foreach ($json['logos'] as $logo) {
|
||||
if (empty($logo['data'])) continue;
|
||||
|
||||
// Decode data
|
||||
$binaryData = base64_decode($logo['data']);
|
||||
if (!$binaryData) continue;
|
||||
|
||||
// Determine filename (try to keep original ID/name or generate new)
|
||||
$extension = $logo['type'] ?? 'png';
|
||||
$filename = $logo['id'] . '.' . $extension;
|
||||
$targetPath = $uploadDir . $filename;
|
||||
|
||||
// Save file
|
||||
if (file_put_contents($targetPath, $binaryData)) {
|
||||
// Update DB
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$db->query("INSERT INTO logos (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size)
|
||||
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,
|
||||
'type' => $extension,
|
||||
'size' => $logo['size']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.restore_success', 'toasts.restore_success_desc', [], true);
|
||||
header('Location: /settings/system');
|
||||
}
|
||||
|
||||
// --- Logo Management ---
|
||||
|
||||
public function logos() {
|
||||
$logoModel = new \App\Models\Logo(); // Fully qualified to avoid import issues for now or add import
|
||||
$logoModel->syncFiles(); // Ensure FS and DB are in sync
|
||||
$logos = $logoModel->getAll();
|
||||
|
||||
// Format size for display (since DB stores raw bytes or maybe we want helper there)
|
||||
// Actually model stored bytes, we format in View or here.
|
||||
// Let's format here for consistency with previous view.
|
||||
foreach ($logos as &$logo) {
|
||||
$logo['formatted_size'] = FormatHelper::formatBytes($logo['size']);
|
||||
}
|
||||
|
||||
return $this->view('settings/logos', ['logos' => $logos]);
|
||||
}
|
||||
|
||||
public function uploadLogo() {
|
||||
if (!isset($_FILES['logo_file'])) {
|
||||
header('Location: /settings/logos');
|
||||
exit;
|
||||
}
|
||||
|
||||
$logoModel = new \App\Models\Logo();
|
||||
try {
|
||||
$logoModel->add($_FILES['logo_file']);
|
||||
} 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('success', 'toasts.logo_uploaded', 'toasts.logo_uploaded_desc', [], true);
|
||||
header('Location: /settings/logos');
|
||||
}
|
||||
|
||||
public function deleteLogo() {
|
||||
$id = $_POST['id']; // Changed from filename to id
|
||||
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$logoModel->delete($id);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.logo_deleted', 'toasts.logo_deleted_desc', [], true);
|
||||
header('Location: /settings/logos');
|
||||
}
|
||||
|
||||
// --- API CORS Management ---
|
||||
|
||||
public function apiCors() {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$rules = $db->query("SELECT * FROM api_cors ORDER BY created_at DESC")->fetchAll();
|
||||
|
||||
// Decode JSON methods and headers for view
|
||||
foreach ($rules as &$rule) {
|
||||
$rule['methods_arr'] = json_decode($rule['methods'], true) ?: [];
|
||||
$rule['headers_arr'] = json_decode($rule['headers'], true) ?: [];
|
||||
}
|
||||
|
||||
return $this->view('settings/api_cors', ['rules' => $rules]);
|
||||
}
|
||||
|
||||
public function storeApiCors() {
|
||||
$origin = $_POST['origin'] ?? '';
|
||||
$methods = isset($_POST['methods']) ? json_encode($_POST['methods']) : '["GET","POST"]';
|
||||
$headers = isset($_POST['headers']) ? json_encode(array_map('trim', explode(',', $_POST['headers']))) : '["*"]';
|
||||
$maxAge = (int)($_POST['max_age'] ?? 3600);
|
||||
|
||||
if (!empty($origin)) {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$db->query("INSERT INTO api_cors (origin, methods, headers, max_age) VALUES (?, ?, ?, ?)", [
|
||||
$origin, $methods, $headers, $maxAge
|
||||
]);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.cors_rule_added', 'toasts.cors_rule_added_desc', ['origin' => $origin], true);
|
||||
}
|
||||
|
||||
header('Location: /settings/api-cors');
|
||||
}
|
||||
|
||||
public function updateApiCors() {
|
||||
$id = $_POST['id'] ?? null;
|
||||
$origin = $_POST['origin'] ?? '';
|
||||
$methods = isset($_POST['methods']) ? json_encode($_POST['methods']) : '["GET","POST"]';
|
||||
$headers = isset($_POST['headers']) ? json_encode(array_map('trim', explode(',', $_POST['headers']))) : '["*"]';
|
||||
$maxAge = (int)($_POST['max_age'] ?? 3600);
|
||||
|
||||
if ($id && !empty($origin)) {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$db->query("UPDATE api_cors SET origin = ?, methods = ?, headers = ?, max_age = ? WHERE id = ?", [
|
||||
$origin, $methods, $headers, $maxAge, $id
|
||||
]);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.cors_rule_updated', 'toasts.cors_rule_updated_desc', ['origin' => $origin], true);
|
||||
}
|
||||
|
||||
header('Location: /settings/api-cors');
|
||||
}
|
||||
|
||||
public function deleteApiCors() {
|
||||
$id = $_POST['id'] ?? null;
|
||||
if ($id) {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$db->query("DELETE FROM api_cors WHERE id = ?", [$id]);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.cors_rule_deleted', 'toasts.cors_rule_deleted_desc', [], true);
|
||||
}
|
||||
header('Location: /settings/api-cors');
|
||||
}
|
||||
}
|
||||
47
app/Controllers/SystemController.php
Normal file
47
app/Controllers/SystemController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
class SystemController extends Controller
|
||||
{
|
||||
// Reboot Router
|
||||
public function reboot($session)
|
||||
{
|
||||
$this->executeCommand($session, '/system/reboot');
|
||||
}
|
||||
|
||||
// Shutdown Router
|
||||
public function shutdown($session)
|
||||
{
|
||||
$this->executeCommand($session, '/system/shutdown');
|
||||
}
|
||||
|
||||
private function executeCommand($session, $command)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
if (!$config) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$API->write($command);
|
||||
// Wait for command to be processed before cutting connection
|
||||
sleep(2);
|
||||
$API->disconnect();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Connection failed']);
|
||||
}
|
||||
}
|
||||
}
|
||||
132
app/Controllers/TemplateController.php
Normal file
132
app/Controllers/TemplateController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\VoucherTemplateModel;
|
||||
use App\Core\Middleware;
|
||||
|
||||
class TemplateController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
Middleware::auth();
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$templateModel = new VoucherTemplateModel();
|
||||
$templates = $templateModel->getAll();
|
||||
|
||||
$data = [
|
||||
'templates' => $templates
|
||||
];
|
||||
return $this->view('settings/templates/index', $data);
|
||||
}
|
||||
|
||||
public function preview($id) {
|
||||
$content = '';
|
||||
if ($id === 'default') {
|
||||
$content = \App\Helpers\TemplateHelper::getDefaultTemplate();
|
||||
} else {
|
||||
$templateModel = new VoucherTemplateModel();
|
||||
$tpl = $templateModel->getById($id);
|
||||
if ($tpl) {
|
||||
$content = $tpl['content'];
|
||||
}
|
||||
}
|
||||
|
||||
echo \App\Helpers\TemplateHelper::getPreviewPage($content);
|
||||
}
|
||||
|
||||
public function add() {
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$logos = $logoModel->getAll();
|
||||
$logoMap = [];
|
||||
foreach ($logos as $l) {
|
||||
$logoMap[$l['id']] = $l['path'];
|
||||
}
|
||||
|
||||
$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)'
|
||||
}
|
||||
|
||||
public function store() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
|
||||
$name = $_POST['name'] ?? 'Untitled';
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
// Session context could be 'global' or specific. For now, let's treat settings templates as global or assign to 'global' session name if column exists.
|
||||
// My migration made 'session_name' NOT NULL.
|
||||
// I will use 'global' for templates created in Settings.
|
||||
|
||||
$data = [
|
||||
'session_name' => 'global',
|
||||
'name' => $name,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
$templateModel = new VoucherTemplateModel();
|
||||
$templateModel->add($data);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.template_created', 'toasts.template_created_desc', ['name' => $name], true);
|
||||
header("Location: /settings/templates");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function edit($id) {
|
||||
$templateModel = new VoucherTemplateModel();
|
||||
$template = $templateModel->getById($id);
|
||||
|
||||
if (!$template) {
|
||||
header("Location: /settings/templates");
|
||||
exit;
|
||||
}
|
||||
|
||||
$logoModel = new \App\Models\Logo();
|
||||
$logos = $logoModel->getAll();
|
||||
$logoMap = [];
|
||||
foreach ($logos as $l) {
|
||||
$logoMap[$l['id']] = $l['path'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'template' => $template,
|
||||
'logoMap' => $logoMap
|
||||
];
|
||||
return $this->view('settings/templates/edit', $data);
|
||||
}
|
||||
|
||||
public function update() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$name = $_POST['name'] ?? '';
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
$templateModel = new VoucherTemplateModel();
|
||||
$templateModel->update($id, $data);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.template_updated', 'toasts.template_updated_desc', ['name' => $name], true);
|
||||
header("Location: /settings/templates");
|
||||
exit;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
$id = $_POST['id'] ?? '';
|
||||
|
||||
$templateModel = new VoucherTemplateModel();
|
||||
$templateModel->delete($id);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.template_deleted', 'toasts.template_deleted_desc', [], true);
|
||||
header("Location: /settings/templates");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
87
app/Controllers/TrafficController.php
Normal file
87
app/Controllers/TrafficController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Models\Config;
|
||||
use App\Libraries\RouterOSAPI;
|
||||
|
||||
class TrafficController extends Controller
|
||||
{
|
||||
public function monitor($session)
|
||||
{
|
||||
// 1. Get Session Config
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Connect to RouterOS
|
||||
$API = new RouterOSAPI();
|
||||
// $API->debug = true;
|
||||
|
||||
// Fast Fail for Traffic Monitor to prevent blocking PHP server
|
||||
$API->attempts = 1;
|
||||
$API->timeout = 2;
|
||||
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
// 3. Get Interface Name from GET param > Config > default 'ether1'
|
||||
$interface = $_GET['interface'] ?? $config['interface'] ?? 'ether1';
|
||||
|
||||
// 4. Fetch Traffic
|
||||
// /interface/monitor-traffic interface=ether1 once
|
||||
$traffic = $API->comm('/interface/monitor-traffic', [
|
||||
"interface" => $interface,
|
||||
"once" => "",
|
||||
]);
|
||||
|
||||
$API->disconnect();
|
||||
|
||||
// 5. Return JSON
|
||||
if (!empty($traffic) && !isset($traffic['!trap'])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($traffic[0]);
|
||||
} else {
|
||||
echo json_encode(['error' => 'No data']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Connection failed']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getInterfaces($session)
|
||||
{
|
||||
// 1. Get Session Config
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Session not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Connect
|
||||
$API = new RouterOSAPI();
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
// 3. Fetch Interfaces
|
||||
// Use comm() to safely handle response parsing and filtering
|
||||
$interfaces = $API->comm('/interface/print', [
|
||||
".proplist" => "name,type"
|
||||
]);
|
||||
$API->disconnect();
|
||||
|
||||
// 4. Return
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($interfaces);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Connection failed']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user