mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3a01aa973 | ||
|
|
ca1fef86bd | ||
|
|
51ca6d3669 | ||
|
|
9cee55c05a | ||
|
|
a0e8c097f7 | ||
|
|
aee64ac137 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ deploy.ps1
|
||||
|
||||
# Plugins
|
||||
/plugins/*
|
||||
!/plugins/.gitkeep
|
||||
@@ -29,8 +29,15 @@ RUN mkdir -p /var/www/html/app/Database && \
|
||||
chown -R www-data:www-data /var/www/html && \
|
||||
chmod -R 755 /var/www/html
|
||||
|
||||
# Copy Entrypoint
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start Supervisor (which starts Nginx & PHP-FPM)
|
||||
# Use Entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
# Start Supervisor
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace App\Config;
|
||||
|
||||
class SiteConfig {
|
||||
const APP_NAME = 'MIVO';
|
||||
const APP_VERSION = 'v1.2.0';
|
||||
const APP_VERSION = 'v1.2.3';
|
||||
const APP_FULL_NAME = 'MIVO - Mikrotik Voucher';
|
||||
const CREDIT_NAME = 'MivoDev';
|
||||
const CREDIT_URL = 'https://github.com/mivodev';
|
||||
|
||||
@@ -87,24 +87,31 @@ class PublicStatusController extends Controller {
|
||||
if (!empty($user)) {
|
||||
$u = $user[0];
|
||||
|
||||
// --- SECURITY CHECK: Hide Unused Vouchers ---
|
||||
// --- SECURITY CHECK: Hide Unused Vouchers (UNLESS ACTIVE) ---
|
||||
$uptimeRaw = $u['uptime'] ?? '0s';
|
||||
$bytesIn = intval($u['bytes-in'] ?? 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();
|
||||
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
|
||||
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;
|
||||
$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.
|
||||
if (!$isActive && $limitBytes === 0 && ($limitUptime === '0s' || empty($limitUptime))) {
|
||||
// Hide unlimited members if they are offline to prevent enumeration
|
||||
$api->disconnect();
|
||||
echo json_encode(['success' => false, 'message' => 'Voucher Not Found']);
|
||||
return;
|
||||
@@ -173,11 +180,9 @@ class PublicStatusController extends Controller {
|
||||
// 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
|
||||
]);
|
||||
// $active already fetched above in Security Check
|
||||
|
||||
if (!empty($active)) {
|
||||
if ($isActive) {
|
||||
$status = 'active';
|
||||
$statusLabel = 'Active (Online)';
|
||||
}
|
||||
|
||||
@@ -27,6 +27,34 @@ class Router {
|
||||
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
|
||||
*/
|
||||
@@ -97,11 +125,17 @@ class Router {
|
||||
|
||||
$path = parse_url($uri, PHP_URL_PATH);
|
||||
|
||||
// Handle subdirectory
|
||||
// Handle subdirectory (SKIP for PHP Built-in Server to avoid SCRIPT_NAME issues)
|
||||
if (php_sapi_name() !== 'cli-server') {
|
||||
$scriptName = dirname($_SERVER['SCRIPT_NAME']);
|
||||
if (strpos($path, $scriptName) === 0) {
|
||||
// Normalize backslashes (Windows)
|
||||
$scriptName = str_replace('\\', '/', $scriptName);
|
||||
|
||||
// Ensure we don't strip root slash
|
||||
if ($scriptName !== '/' && strpos($path, $scriptName) === 0) {
|
||||
$path = substr($path, strlen($scriptName));
|
||||
}
|
||||
}
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
// Global Install Check
|
||||
|
||||
@@ -40,6 +40,6 @@ class LanguageHelper
|
||||
}
|
||||
}
|
||||
|
||||
return $languages;
|
||||
return \App\Core\Hooks::applyFilters('get_available_languages', $languages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://github.com/mivodev/mivo/issues" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://github.com/mivodev/mivo/discussions" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span>Community</span>
|
||||
</a>
|
||||
@@ -30,6 +30,10 @@
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
window.MIVO_VERSION = "<?= \App\Config\SiteConfig::APP_VERSION ?>";
|
||||
</script>
|
||||
<script src="/assets/js/modules/update-checker.js"></script>
|
||||
<script>
|
||||
// Global Theme Toggle Logic (Class-based for multiple instances)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://github.com/mivodev/mivo/issues" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://github.com/mivodev/mivo/discussions" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span>Community</span>
|
||||
</a>
|
||||
|
||||
@@ -53,8 +53,9 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
<?php
|
||||
$languages = \App\Helpers\LanguageHelper::getAvailableLanguages();
|
||||
foreach ($languages as $lang):
|
||||
$pathArg = isset($lang['path']) ? "', '" . $lang['path'] : "";
|
||||
?>
|
||||
<button onclick="Mivo.modules.I18n.loadLanguage('<?= $lang['code'] ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang">
|
||||
<button onclick="Mivo.modules.I18n.loadLanguage('<?= $lang['code'] ?><?= $pathArg ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-sm shadow-sm transition-transform group-hover/lang:scale-110"></span>
|
||||
<span><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
@@ -123,8 +124,10 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
<span class="text-xs font-bold text-accents-4 uppercase tracking-wider">Select Language</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide snap-x">
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?>')" class="flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full border border-accents-2 bg-background hover:border-foreground transition-all text-sm font-medium snap-start shadow-sm">
|
||||
<?php foreach ($languages as $lang):
|
||||
$pathArg = isset($lang['path']) ? "', '" . $lang['path'] : "";
|
||||
?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?><?= $pathArg ?>')" class="flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full border border-accents-2 bg-background hover:border-foreground transition-all text-sm font-medium snap-start shadow-sm">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-full shadow-sm"></span>
|
||||
<span class="whitespace-nowrap"><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
|
||||
@@ -397,7 +397,7 @@ $getInitials = function($name) {
|
||||
</a>
|
||||
|
||||
<!-- Community -->
|
||||
<a href="https://github.com/mivodev/mivo/issues" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<a href="https://github.com/mivodev/mivo/discussions" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.community">Community</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
|
||||
25
docker/entrypoint.sh
Normal file
25
docker/entrypoint.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Ensure Database directory exists
|
||||
mkdir -p /var/www/html/app/Database
|
||||
|
||||
# Fix permissions for the Database directory
|
||||
# This is crucial for SQLite when volumes are mounted from host
|
||||
if [ -d "/var/www/html/app/Database" ]; then
|
||||
chown -R www-data:www-data /var/www/html/app/Database
|
||||
chmod -R 775 /var/www/html/app/Database
|
||||
fi
|
||||
|
||||
# Also ensure .env is writable if it exists, or create it from example
|
||||
if [ ! -f "/var/www/html/.env" ] && [ -f "/var/www/html/.env.example" ]; then
|
||||
cp /var/www/html/.env.example /var/www/html/.env
|
||||
chown www-data:www-data /var/www/html/.env
|
||||
fi
|
||||
|
||||
if [ -f "/var/www/html/.env" ]; then
|
||||
chmod 664 /var/www/html/.env
|
||||
fi
|
||||
|
||||
# Execute the command passed to docker run (usually supervisor)
|
||||
exec "$@"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mivo",
|
||||
"version": "1.2.0",
|
||||
"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).",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
0
plugins/.gitkeep
Normal file
0
plugins/.gitkeep
Normal file
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Core\Hooks;
|
||||
use App\Core\Router;
|
||||
|
||||
// 1. Hook into Router to add a custom page
|
||||
Hooks::addAction('router_init', function(Router $router) {
|
||||
$router->get('/plugin-test', function() {
|
||||
echo "<h1>Hello from Example Plugin!</h1>";
|
||||
echo "<p>This page is registered via <code>router_init</code> hook.</p>";
|
||||
echo "<a href='/'>Back to Home</a>";
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Hook into Head to add custom CSS
|
||||
Hooks::addAction('mivo_head', function() {
|
||||
echo "<!-- Example Plugin CSS -->";
|
||||
echo "<style>body { border-top: 5px solid #10b981; }</style>";
|
||||
});
|
||||
|
||||
// 3. Hook into Footer to add custom JS or Text
|
||||
Hooks::addAction('mivo_footer', function() {
|
||||
echo "<!-- Example Plugin Footer -->";
|
||||
echo "<div style='text-align:center; padding: 10px; background: #f0fdf4; color: #166534; font-weight: bold;'>
|
||||
Plugin System is Working! 🚀
|
||||
</div>";
|
||||
});
|
||||
@@ -19,10 +19,12 @@ class I18n {
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
async loadLanguage(lang) {
|
||||
async loadLanguage(lang, customUrl = null) {
|
||||
try {
|
||||
const cacheBuster = Date.now();
|
||||
const response = await fetch(`/lang/${lang}.json?v=${cacheBuster}`);
|
||||
// Use custom URL if provided, otherwise default to public/lang structure
|
||||
const url = customUrl || `/lang/${lang}.json`;
|
||||
const response = await fetch(`${url}?v=${cacheBuster}`);
|
||||
if (!response.ok) throw new Error(`Failed to load language: ${lang}`);
|
||||
|
||||
this.translations = await response.json();
|
||||
|
||||
108
public/assets/js/modules/update-checker.js
Normal file
108
public/assets/js/modules/update-checker.js
Normal 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();
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"name": "English",
|
||||
"flag": "gb"
|
||||
"flag": "us"
|
||||
},
|
||||
"common": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -11,9 +11,11 @@ $router->group(['middleware' => 'cors'], function($router) {
|
||||
|
||||
// Public Status API (No Auth Check in Controller)
|
||||
$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)
|
||||
$router->post('/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
|
||||
|
||||
});
|
||||
|
||||
@@ -39,6 +39,10 @@ $router->group(['middleware' => 'router.valid'], function($router) {
|
||||
// Temporary Test Route
|
||||
$router->get('/test-alert', [HomeController::class, 'testAlert']);
|
||||
|
||||
// Plugin Language Route - DEPRECATED
|
||||
// Plugins now handle their own routing via Hooks::addAction('router_init')
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Protected Admin Routes (Requires Auth)
|
||||
|
||||
Reference in New Issue
Block a user