Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack

This commit is contained in:
dyzulk
2026-01-16 11:21:32 +07:00
commit 45623973a8
139 changed files with 24302 additions and 0 deletions

171
app/Models/Config.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
namespace App\Models;
class Config {
protected $configPath;
public function __construct() {
// MIVO Standalone Config Path
// Points to /include/config.php within the MIVO directory
$this->configPath = ROOT . '/include/config.php';
}
public function getSession($sessionName) {
// 1. Check SQLite Database First
try {
$db = \App\Core\Database::getInstance();
$stmt = $db->query("SELECT * FROM routers WHERE session_name = ?", [$sessionName]);
$router = $stmt->fetch();
if ($router) {
return [
'ip' => $router['ip_address'],
'ip_address' => $router['ip_address'], // Alias
'user' => $router['username'],
'username' => $router['username'], // Alias
'password' => \App\Helpers\EncryptionHelper::decrypt($router['password']),
'hotspot_name' => $router['hotspot_name'],
'dns_name' => $router['dns_name'],
'currency' => $router['currency'],
'reload' => $router['reload_interval'],
'interface' => $router['interface'],
'info' => $router['description'],
'quick_access' => $router['quick_access'] ?? 0,
'source' => 'sqlite'
];
}
} catch (\Exception $e) {
// Ignore DB error and fallback
}
// 2. Fallback to Legacy Config
if (!file_exists($this->configPath)) {
return null;
}
include $this->configPath;
if (isset($data) && isset($data[$sessionName]) && is_array($data[$sessionName])) {
$s = $data[$sessionName];
return [
'ip' => isset($s[1]) ? explode('!', $s[1])[1] : '',
'ip_address' => isset($s[1]) ? explode('!', $s[1])[1] : '', // Alias
'user' => isset($s[2]) ? explode('@|@', $s[2])[1] : '',
'username' => isset($s[2]) ? explode('@|@', $s[2])[1] : '', // Alias
'password' => isset($s[3]) ? explode('#|#', $s[3])[1] : '',
'hotspot_name' => isset($s[4]) ? explode('%', $s[4])[1] : '',
'dns_name' => isset($s[5]) ? explode('^', $s[5])[1] : '',
'currency' => isset($s[6]) ? explode('&', $s[6])[1] : '',
'reload' => isset($s[7]) ? explode('*', $s[7])[1] : '',
'interface' => isset($s[8]) ? explode('(', $s[8])[1] : '',
'info' => isset($s[9]) ? explode(')', $s[9])[1] : '',
'source' => 'legacy'
];
}
return null;
}
public function getAllSessions() {
// SQLite
try {
$db = \App\Core\Database::getInstance();
$stmt = $db->query("SELECT * FROM routers");
return $stmt->fetchAll();
} catch (\Exception $e) {
return [];
}
}
public function getSessionById($id) {
$db = \App\Core\Database::getInstance();
$stmt = $db->query("SELECT * FROM routers WHERE id = ?", [$id]);
$router = $stmt->fetch();
if ($router) {
return [
'id' => $router['id'],
'session_name' => $router['session_name'],
'ip_address' => $router['ip_address'],
'username' => $router['username'],
'password' => \App\Helpers\EncryptionHelper::decrypt($router['password']),
'hotspot_name' => $router['hotspot_name'],
'dns_name' => $router['dns_name'],
'currency' => $router['currency'],
'reload_interval' => $router['reload_interval'],
'interface' => $router['interface'],
'interface' => $router['interface'],
'description' => $router['description'],
'quick_access' => $router['quick_access'] ?? 0
];
}
return null;
}
public function addSession($data) {
$db = \App\Core\Database::getInstance();
$sql = "INSERT INTO routers (session_name, ip_address, username, password, hotspot_name, dns_name, currency, reload_interval, interface, description, quick_access)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
return $db->query($sql, [
$data['session_name'] ?? 'New Session',
$data['ip_address'] ?? '',
$data['username'] ?? '',
\App\Helpers\EncryptionHelper::encrypt($data['password'] ?? ''),
$data['hotspot_name'] ?? '',
$data['dns_name'] ?? '',
$data['currency'] ?? 'RP',
$data['reload_interval'] ?? 60,
$data['interface'] ?? 'ether1',
$data['description'] ?? '',
$data['quick_access'] ?? 0
]);
}
public function updateSession($id, $data) {
$db = \App\Core\Database::getInstance();
// If password is provided, encrypt it. If empty, don't update it (keep existing).
if (!empty($data['password'])) {
$sql = "UPDATE routers SET session_name=?, ip_address=?, username=?, password=?, hotspot_name=?, dns_name=?, currency=?, reload_interval=?, interface=?, description=?, quick_access=? WHERE id=?";
$params = [
$data['session_name'],
$data['ip_address'],
$data['username'],
\App\Helpers\EncryptionHelper::encrypt($data['password']),
$data['hotspot_name'],
$data['dns_name'],
$data['currency'],
$data['reload_interval'],
$data['interface'],
$data['description'],
$data['quick_access'] ?? 0,
$id
];
} else {
$sql = "UPDATE routers SET session_name=?, ip_address=?, username=?, hotspot_name=?, dns_name=?, currency=?, reload_interval=?, interface=?, description=?, quick_access=? WHERE id=?";
$params = [
$data['session_name'],
$data['ip_address'],
$data['username'],
$data['hotspot_name'],
$data['dns_name'],
$data['currency'],
$data['reload_interval'],
$data['interface'],
$data['description'],
$data['quick_access'] ?? 0,
$id
];
}
return $db->query($sql, $params);
}
public function deleteSession($id) {
$db = \App\Core\Database::getInstance();
return $db->query("DELETE FROM routers WHERE id = ?", [$id]);
}
}

145
app/Models/Logo.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
namespace App\Models;
use PDO;
use Exception;
class Logo {
protected $db;
protected $table = 'logos';
public function __construct() {
$this->db = \App\Core\Database::getInstance();
$this->initTable();
}
// Connect method removed as we use shared instance
private function initTable() {
$query = "CREATE TABLE IF NOT EXISTS {$this->table} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL,
type TEXT,
size INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)";
$this->db->query($query);
}
public function generateId($length = 6) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
public function getAll() {
$stmt = $this->db->query("SELECT * FROM {$this->table} ORDER BY created_at DESC");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getById($id) {
$stmt = $this->db->query("SELECT * FROM {$this->table} WHERE id = :id", ['id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function add($file) {
// Security: Strict MIME Type Check
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
$allowedMimes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/svg+xml' => 'svg',
'image/webp' => 'webp'
];
if (!array_key_exists($mimeType, $allowedMimes)) {
throw new Exception("Invalid file type: " . $mimeType);
}
// Use extension mapped from MIME type or sanitize original
// Better to trust MIME mapping for extensions to avoid double extension attacks
$extension = $allowedMimes[$mimeType];
// Generate Unique Short ID
do {
$id = $this->generateId();
$exists = $this->getById($id);
} while ($exists);
$uploadDir = ROOT . '/public/assets/img/logos/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$filename = $id . '.' . $extension;
$targetPath = $uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$this->db->query("INSERT INTO {$this->table} (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size)", [
'id' => $id,
'name' => $file['name'],
'path' => '/assets/img/logos/' . $filename,
'type' => $extension,
'size' => $file['size']
]);
return $id;
}
return false;
}
public function syncFiles() {
// One-time sync: scan folder, if file not in DB, add it.
$logoDir = ROOT . '/public/assets/img/logos/';
if (!file_exists($logoDir)) return;
$files = glob($logoDir . '*.{jpg,jpeg,png,gif,svg}', GLOB_BRACE);
foreach ($files as $file) {
$filename = basename($file);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
// Check if file is registered (maybe by path match)
$webPath = '/assets/img/logos/' . $filename;
$stmt = $this->db->query("SELECT COUNT(*) FROM {$this->table} WHERE path = :path", ['path' => $webPath]);
if ($stmt->fetchColumn() == 0) {
// Not in DB, register it.
// Ideally we'd rename it to a hashID, but since it's existing, let's generate an ID and map it.
do {
$id = $this->generateId();
$exists = $this->getById($id);
} while ($exists);
$this->db->query("INSERT INTO {$this->table} (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size)", [
'id' => $id,
'name' => $filename,
'path' => $webPath,
'type' => $extension,
'size' => filesize($file)
]);
}
}
}
public function delete($id) {
$logo = $this->getById($id);
if ($logo) {
$filePath = ROOT . '/public' . $logo['path'];
if (file_exists($filePath)) {
unlink($filePath);
}
$this->db->query("DELETE FROM {$this->table} WHERE id = :id", ['id' => $id]);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Models;
use App\Core\Database;
class QuickPrintModel {
public function getAllBySession($sessionName) {
$db = Database::getInstance();
$stmt = $db->query("SELECT * FROM quick_prints WHERE session_name = ?", [$sessionName]);
return $stmt->fetchAll();
}
public function getById($id) {
$db = Database::getInstance();
$stmt = $db->query("SELECT * FROM quick_prints WHERE id = ?", [$id]);
return $stmt->fetch();
}
public function add($data) {
$db = Database::getInstance();
$sql = "INSERT INTO quick_prints (session_name, name, server, profile, prefix, char_length, price, time_limit, data_limit, comment, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
return $db->query($sql, [
$data['session_name'],
$data['name'],
$data['server'],
$data['profile'],
$data['prefix'] ?? '',
$data['char_length'] ?? 4,
$data['price'] ?? 0,
$data['time_limit'] ?? '',
$data['data_limit'] ?? '',
$data['comment'] ?? '',
$data['color'] ?? 'bg-blue-500'
]);
}
public function update($id, $data) {
$db = Database::getInstance();
$sql = "UPDATE quick_prints SET name=?, server=?, profile=?, prefix=?, char_length=?, price=?, time_limit=?, data_limit=?, comment=?, color=?, updated_at=CURRENT_TIMESTAMP WHERE id=?";
return $db->query($sql, [
$data['name'],
$data['server'],
$data['profile'],
$data['prefix'] ?? '',
$data['char_length'] ?? 4,
$data['price'] ?? 0,
$data['time_limit'] ?? '',
$data['data_limit'] ?? '',
$data['comment'] ?? '',
$data['color'] ?? 'bg-blue-500',
$id
]);
}
public function delete($id) {
$db = Database::getInstance();
return $db->query("DELETE FROM quick_prints WHERE id = ?", [$id]);
}
}

46
app/Models/Setting.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Core\Database;
class Setting {
private $db;
private $table = 'settings';
public function __construct() {
$this->db = Database::getInstance();
$this->initTable();
}
private function initTable() {
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
key TEXT PRIMARY KEY,
value TEXT
)";
$this->db->query($sql);
}
public function get($key, $default = null) {
$stmt = $this->db->query("SELECT value FROM {$this->table} WHERE key = ?", [$key]);
$row = $stmt->fetch();
return $row ? $row['value'] : $default;
}
public function set($key, $value) {
// SQLite Upsert
$sql = "INSERT INTO {$this->table} (key, value) VALUES (:key, :value)
ON CONFLICT(key) DO UPDATE SET value = excluded.value";
return $this->db->query($sql, ['key' => $key, 'value' => $value]);
}
public function getAll() {
$stmt = $this->db->query("SELECT * FROM {$this->table}");
$results = $stmt->fetchAll();
$settings = [];
foreach ($results as $row) {
$settings[$row['key']] = $row['value'];
}
return $settings;
}
}

24
app/Models/User.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use App\Core\Database;
class User {
private $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function attempt($username, $password) {
$stmt = $this->db->query("SELECT * FROM users WHERE username = ?", [$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
return $user;
}
return false;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Models;
use App\Core\Database;
class VoucherTemplateModel {
public function getAll() {
$db = Database::getInstance();
$stmt = $db->query("SELECT * FROM voucher_templates");
return $stmt->fetchAll();
}
public function getBySession($sessionName) {
// Templates can be global or session specific, but allow session filtering
$db = Database::getInstance();
$stmt = $db->query("SELECT * FROM voucher_templates WHERE session_name = ? OR session_name = 'global'", [$sessionName]);
return $stmt->fetchAll();
}
public function getById($id) {
$db = Database::getInstance();
$stmt = $db->query("SELECT * FROM voucher_templates WHERE id = ?", [$id]);
return $stmt->fetch();
}
public function add($data) {
$db = Database::getInstance();
$sql = "INSERT INTO voucher_templates (session_name, name, content) VALUES (?, ?, ?)";
return $db->query($sql, [
$data['session_name'],
$data['name'],
$data['content']
]);
}
public function update($id, $data) {
$db = Database::getInstance();
$sql = "UPDATE voucher_templates SET name=?, content=?, updated_at=CURRENT_TIMESTAMP WHERE id=?";
return $db->query($sql, [
$data['name'],
$data['content'],
$id
]);
}
public function delete($id) {
$db = Database::getInstance();
return $db->query("DELETE FROM voucher_templates WHERE id = ?", [$id]);
}
}