2 Commits

Author SHA1 Message Date
dyzulk
f3a01aa973 feat: v1.2.3 release - status bars, cors fix, update checker 2026-01-19 13:59:46 +07:00
dyzulk
ca1fef86bd chore: bump version to v1.2.3 - Fix CORS & Update Notification 2026-01-19 13:29:20 +07:00
7 changed files with 160 additions and 13 deletions

View File

@@ -3,7 +3,7 @@ namespace App\Config;
class SiteConfig { class SiteConfig {
const APP_NAME = 'MIVO'; const APP_NAME = 'MIVO';
const APP_VERSION = 'v1.2.2'; const APP_VERSION = 'v1.2.3';
const APP_FULL_NAME = 'MIVO - Mikrotik Voucher'; const APP_FULL_NAME = 'MIVO - Mikrotik Voucher';
const CREDIT_NAME = 'MivoDev'; const CREDIT_NAME = 'MivoDev';
const CREDIT_URL = 'https://github.com/mivodev'; const CREDIT_URL = 'https://github.com/mivodev';

View File

@@ -87,24 +87,31 @@ class PublicStatusController extends Controller {
if (!empty($user)) { if (!empty($user)) {
$u = $user[0]; $u = $user[0];
// --- SECURITY CHECK: Hide Unused Vouchers --- // --- SECURITY CHECK: Hide Unused Vouchers (UNLESS ACTIVE) ---
$uptimeRaw = $u['uptime'] ?? '0s'; $uptimeRaw = $u['uptime'] ?? '0s';
$bytesIn = intval($u['bytes-in'] ?? 0); $bytesIn = intval($u['bytes-in'] ?? 0);
$bytesOut = intval($u['bytes-out'] ?? 0); $bytesOut = intval($u['bytes-out'] ?? 0);
if (($uptimeRaw === '0s' || empty($uptimeRaw)) && ($bytesIn + $bytesOut) === 0) { // Check if active first
$active = $api->comm("/ip/hotspot/active/print", [
"?user" => $code
]);
$isActive = !empty($active);
// If Empty Stats AND Not Active => Hide (It's an unused new voucher)
// If Empty Stats BUT Active => Show! (It's a fresh session)
if (!$isActive && ($uptimeRaw === '0s' || empty($uptimeRaw)) && ($bytesIn + $bytesOut) === 0) {
$api->disconnect(); $api->disconnect();
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']); echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
return; return;
} }
// --- SECURITY CHECK: Hide Unlimited Members --- // --- SECURITY CHECK: Hide Unlimited Members (UNLESS ACTIVE) ---
$limitBytes = isset($u['limit-bytes-total']) ? intval($u['limit-bytes-total']) : 0; $limitBytes = isset($u['limit-bytes-total']) ? intval($u['limit-bytes-total']) : 0;
$limitUptime = $u['limit-uptime'] ?? '0s'; $limitUptime = $u['limit-uptime'] ?? '0s';
if ($limitBytes === 0 && ($limitUptime === '0s' || empty($limitUptime))) { if (!$isActive && $limitBytes === 0 && ($limitUptime === '0s' || empty($limitUptime))) {
// Option: Allow checking them but show minimalistic info, or hide. // Hide unlimited members if they are offline to prevent enumeration
// Sticking to original logic: Hide them.
$api->disconnect(); $api->disconnect();
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']); echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
return; return;
@@ -173,11 +180,9 @@ class PublicStatusController extends Controller {
// 2. CHECK ACTIVE OVERRIDE // 2. CHECK ACTIVE OVERRIDE
// If user is conceptually valid (or even if limited?), check if they are currently active // 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) // 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", [ // $active already fetched above in Security Check
"?user" => $code
]);
if (!empty($active)) { if ($isActive) {
$status = 'active'; $status = 'active';
$statusLabel = 'Active (Online)'; $statusLabel = 'Active (Online)';
} }

View File

@@ -27,6 +27,34 @@ class Router {
return $this->addRoute('POST', $path, $callback); return $this->addRoute('POST', $path, $callback);
} }
/**
* Add a OPTIONS route (Crucial for CORS Preflight)
*/
public function options($path, $callback) {
return $this->addRoute('OPTIONS', $path, $callback);
}
/**
* Add a PUT route
*/
public function put($path, $callback) {
return $this->addRoute('PUT', $path, $callback);
}
/**
* Add a PATCH route
*/
public function patch($path, $callback) {
return $this->addRoute('PATCH', $path, $callback);
}
/**
* Add a DELETE route
*/
public function delete($path, $callback) {
return $this->addRoute('DELETE', $path, $callback);
}
/** /**
* Add route to collection and return $this for chaining * Add route to collection and return $this for chaining
*/ */

View File

@@ -30,6 +30,10 @@
</footer> </footer>
<?php endif; ?> <?php endif; ?>
<script>
window.MIVO_VERSION = "<?= \App\Config\SiteConfig::APP_VERSION ?>";
</script>
<script src="/assets/js/modules/update-checker.js"></script>
<script> <script>
// Global Theme Toggle Logic (Class-based for multiple instances) // Global Theme Toggle Logic (Class-based for multiple instances)
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

View File

@@ -1,6 +1,6 @@
{ {
"name": "mivo", "name": "mivo",
"version": "1.2.2", "version": "1.2.3",
"description": "This is a complete rewrite of Mivo using a modern MVC architecture.\r It runs on a lightweight custom core designed for performance on low-end devices (STB/Android).", "description": "This is a complete rewrite of Mivo using a modern MVC architecture.\r It runs on a lightweight custom core designed for performance on low-end devices (STB/Android).",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -0,0 +1,108 @@
/**
* Mivo Update Checker
* Checks GitHub for latest release and notifies user.
* Caches result for 24 hours to avoid rate limits.
*/
document.addEventListener('DOMContentLoaded', () => {
const REPO = 'mivodev/mivo';
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 Hours
const CACHE_KEY = 'mivo_update_cache';
// UI Elements
const badge = document.getElementById('update-badge');
const content = document.getElementById('notification-content');
const dropdown = document.getElementById('notification-dropdown');
if (!badge || !content) return;
// Current Version from PHP (injected in footer)
const currentVersion = window.MIVO_VERSION || 'v0.0.0';
const checkUpdate = async () => {
try {
// Check Cache
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const data = JSON.parse(cached);
const age = Date.now() - data.timestamp;
if (age < CHECK_INTERVAL && data.version) {
processUpdate(data);
return;
}
}
// Fetch from GitHub
// console.log('Checking for updates...');
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`);
if (!res.ok) throw new Error('GitHub API Error');
const json = await res.json();
const latestVersion = json.tag_name; // e.g., "v1.3.0"
const body = json.body || 'No release notes.';
const htmlUrl = json.html_url;
const cacheData = {
version: latestVersion,
body: body,
url: htmlUrl,
timestamp: Date.now()
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
processUpdate(cacheData);
} catch (error) {
console.warn('Update check failed:', error);
}
};
const processUpdate = (data) => {
// Simple string comparison for now. Ideally semver.
if (data.version !== currentVersion && compareVersions(data.version, currentVersion) > 0) {
// Show Badge
badge.classList.remove('hidden');
// Update Dropdown Content
content.innerHTML = `
<div class="text-left space-y-2">
<div class="flex items-center justify-between">
<span class="font-bold text-foreground">New Version Available!</span>
<span class="text-xs bg-accents-2 px-1.5 py-0.5 rounded text-foreground">${data.version}</span>
</div>
<p class="text-xs text-accents-5 line-clamp-3">${data.body.substring(0, 100)}...</p>
<a href="${data.url}" target="_blank" class="block w-full text-center px-3 py-2 bg-foreground text-background font-bold rounded-lg text-xs hover:bg-foreground/90 transition-colors mt-2">
View Release
</a>
</div>
`;
} else {
// Up to date
content.innerHTML = `
<div class="py-2">
<i data-lucide="check-circle" class="w-8 h-8 text-emerald-500 mx-auto mb-2"></i>
<p>You are using the latest version.</p>
<p class="text-xs text-accents-4 mt-1">Current: ${currentVersion}</p>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
};
// Helper: Compare "v1.2.0" vs "v1.1.0"
const compareVersions = (v1, v2) => {
const clean = v => v.replace(/^v/, '').split('.').map(Number);
const a = clean(v1);
const b = clean(v2);
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const valA = a[i] || 0;
const valB = b[i] || 0;
if (valA > valB) return 1;
if (valA < valB) return -1;
}
return 0;
};
// Init
checkUpdate();
});

View File

@@ -11,9 +11,11 @@ $router->group(['middleware' => 'cors'], function($router) {
// Public Status API (No Auth Check in Controller) // Public Status API (No Auth Check in Controller)
$router->post('/api/status/check', [App\Controllers\PublicStatusController::class, 'check']); $router->post('/api/status/check', [App\Controllers\PublicStatusController::class, 'check']);
$router->options('/api/status/check', function() { return; });
// Voucher Check (Code/Username in URL) - Support GET (Status Page) and POST (Login Page Check) // Voucher Check (Code/Username in URL) - Support GET (Status Page) and POST (Login Page Check)
$router->post('/api/voucher/check/{code}', [App\Controllers\PublicStatusController::class, 'check']); $router->post('/api/voucher/check/{code}', [App\Controllers\PublicStatusController::class, 'check']);
$router->get('/api/voucher/check/{code}', [App\Controllers\PublicStatusController::class, 'check']); $router->get('/api/voucher/check/{code}', [App\Controllers\PublicStatusController::class, 'check']);
$router->options('/api/voucher/check/{code}', function() { return; }); // CORS Middleware handles this
}); });