fix: PHP frontend JWT auth + API endpoints + missing tables + branding
- login.php: Fix JWT response format (success/access_token vs status=ok), store JWT tokens in session, support TOTP 202 flow - api_client.php: Dual auth (JWT Bearer + X-API-Key fallback), add auto-refresh on 401, fix API endpoints (checkServerHealth → array, getServerLogs, getAuditLogs, getDashboardStats field mapping) - api_proxy.php: Fix check_all to use /api/servers/check (not per-server), fix get_logs to support global endpoint, restart_multisync → restart_nexus - install.php: Add 4 missing tables (platforms, nodes, ssh_sessions, command_logs), fix table creation order for FK dependencies, add JWT columns to admins, add new indexes, add platform_id/node_id/protocols/ extra_attrs/connectivity to servers - logout.php: Call /api/auth/logout to invalidate JWT refresh token - index.php: Use JWT Bearer auth for API calls - server_service.py: Implement real check_all_servers with concurrent httpx Agent health checks (was stub) - servers.py: Add GET /api/servers/logs global sync logs endpoint - Branding: MultiSync → Nexus across all PHP files (sidebar, titles, TOTP issuer, version strings) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -13,9 +13,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from server.api.dependencies import get_server_service, get_sync_service, get_db
|
||||
from server.application.services.server_service import ServerService
|
||||
from server.application.services.sync_service import SyncService
|
||||
from server.domain.models import Server
|
||||
from server.domain.models import Server, SyncLog
|
||||
from server.infrastructure.redis.client import get_redis
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger("nexus.servers")
|
||||
@@ -221,6 +223,19 @@ async def push_to_servers(
|
||||
|
||||
# ── Sync Logs ──
|
||||
|
||||
@router.get("/logs", response_model=list)
|
||||
async def get_all_sync_logs(
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
):
|
||||
"""Get recent sync logs across all servers (for dashboard charts)"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(SyncLog).order_by(SyncLog.started_at.desc()).limit(limit)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
return [_sync_log_to_dict(log) for log in logs]
|
||||
|
||||
|
||||
@router.get("/{id}/logs", response_model=list)
|
||||
async def get_server_logs(
|
||||
id: int,
|
||||
|
||||
@@ -48,17 +48,38 @@ class ServerService:
|
||||
await self.server_repo.update_heartbeat(id, is_online, system_info, agent_version)
|
||||
|
||||
async def check_all_servers(self, server_ids: List[int]) -> Dict[int, dict]:
|
||||
"""Check health of multiple servers via Agent API (no SSH)"""
|
||||
"""Check health of multiple servers via Agent API (async concurrent)"""
|
||||
import httpx
|
||||
results = {}
|
||||
# TODO: Use httpx to call each Agent's /health endpoint
|
||||
for id in server_ids:
|
||||
server = await self.server_repo.get_by_id(id)
|
||||
if server and server.is_online:
|
||||
results[id] = {"status": "online", "system_info": json.loads(server.system_info or "{}")}
|
||||
elif server:
|
||||
results[id] = {"status": "offline"}
|
||||
|
||||
async def _check_one(server: Server) -> tuple:
|
||||
url = f"http://{server.domain}:{server.agent_port}/health"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
headers = {}
|
||||
if server.agent_api_key:
|
||||
headers["X-API-Key"] = server.agent_api_key
|
||||
resp = await client.get(url, headers=headers)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
return server.id, {"status": "online", "system_info": data}
|
||||
return server.id, {"status": "offline", "error": f"HTTP {resp.status_code}"}
|
||||
except Exception as e:
|
||||
return server.id, {"status": "offline", "error": str(e)[:200]}
|
||||
|
||||
servers = []
|
||||
for sid in server_ids:
|
||||
server = await self.server_repo.get_by_id(sid)
|
||||
if server:
|
||||
servers.append(server)
|
||||
else:
|
||||
results[id] = {"status": "not_found"}
|
||||
results[sid] = {"status": "not_found"}
|
||||
|
||||
if servers:
|
||||
check_results = await asyncio.gather(*[_check_one(s) for s in servers])
|
||||
for sid, result in check_results:
|
||||
results[sid] = result
|
||||
|
||||
return results
|
||||
|
||||
async def push_to_servers(
|
||||
|
||||
@@ -8,7 +8,7 @@ $current_page = basename($_SERVER['SCRIPT_NAME']);
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $pageTitle ?? 'MultiSync' ?></title>
|
||||
<title><?= $pageTitle ?? 'Nexus' ?></title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ $current_page = basename($_SERVER['SCRIPT_NAME']);
|
||||
?>
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1><i class="fas fa-sync-alt"></i> MultiSync</h1>
|
||||
<span class="version">v1.0.0</span>
|
||||
<h1><i class="fas fa-network-wired"></i> Nexus</h1>
|
||||
<span class="version">v6.0.0</span>
|
||||
</div>
|
||||
<ul class="nav-menu">
|
||||
<li class="nav-section-title">主菜单</li>
|
||||
|
||||
@@ -24,7 +24,7 @@ $sysMenu = [
|
||||
</nav>
|
||||
|
||||
<aside class="main-sidebar sidebar-dark-primary elevation-4">
|
||||
<a href="index.php" class="brand-link"><i class="fas fa-sync-alt brand-image"></i><span class="brand-text font-weight-light">MultiSync</span></a>
|
||||
<a href="index.php" class="brand-link"><i class="fas fa-sync-alt brand-image"></i><span class="brand-text font-weight-light">Nexus</span></a>
|
||||
<div class="sidebar">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu">
|
||||
|
||||
+72
-22
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* API Client - communicates with FastAPI backend
|
||||
* Supports both JWT Bearer (admin session) and X-API-Key (agent/service) auth.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
@@ -8,10 +9,16 @@ require_once __DIR__ . '/config.php';
|
||||
class ApiClient {
|
||||
private $baseUrl;
|
||||
private $apiKey;
|
||||
private $accessToken;
|
||||
|
||||
public function __construct() {
|
||||
$this->baseUrl = API_BASE_URL;
|
||||
// Read API key from MySQL first (settings page), fallback to config constant
|
||||
|
||||
// JWT token from session (admin login) takes priority
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
$this->accessToken = $_SESSION['access_token'] ?? '';
|
||||
|
||||
// Fallback: API key from MySQL settings, then config constant
|
||||
$key = defined('API_KEY') ? API_KEY : '';
|
||||
try {
|
||||
require_once __DIR__ . '/db.php';
|
||||
@@ -25,20 +32,28 @@ class ApiClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
* Make API request — uses JWT Bearer if available, else X-API-Key
|
||||
*/
|
||||
public function request($method, $endpoint, $data = null) {
|
||||
$url = $this->baseUrl . $endpoint;
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
|
||||
// JWT Bearer takes priority (admin session), X-API-Key as fallback (agent/service)
|
||||
if (!empty($this->accessToken)) {
|
||||
$headers[] = 'Authorization: Bearer ' . $this->accessToken;
|
||||
} else {
|
||||
$headers[] = 'X-API-Key: ' . $this->apiKey;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
'X-API-Key: ' . $this->apiKey,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
switch (strtoupper($method)) {
|
||||
case 'POST':
|
||||
@@ -67,10 +82,48 @@ class ApiClient {
|
||||
return ['error' => "Connection error: $error", 'code' => 0];
|
||||
}
|
||||
|
||||
// JWT expired — try refresh
|
||||
if ($httpCode === 401 && !empty($_SESSION['refresh_token'])) {
|
||||
$refreshed = $this->_refreshToken();
|
||||
if ($refreshed) {
|
||||
return $this->request($method, $endpoint, $data);
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
return ['data' => $decoded, 'code' => $httpCode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh JWT tokens using refresh_token
|
||||
*/
|
||||
private function _refreshToken() {
|
||||
try {
|
||||
$ch = curl_init($this->baseUrl . '/api/auth/refresh');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode(['refresh_token' => $_SESSION['refresh_token']]),
|
||||
]);
|
||||
$resp = json_decode(curl_exec($ch), true);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code === 200 && ($resp['success'] ?? false)) {
|
||||
$_SESSION['access_token'] = $resp['access_token'];
|
||||
$_SESSION['refresh_token'] = $resp['refresh_token'] ?? $_SESSION['refresh_token'];
|
||||
$_SESSION['token_expires'] = time() + ($resp['expires_in'] ?? 1800);
|
||||
$this->accessToken = $resp['access_token'];
|
||||
return true;
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
// Refresh failed — clear session, force re-login
|
||||
unset($_SESSION['access_token'], $_SESSION['refresh_token'], $_SESSION['token_expires']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Servers ---
|
||||
|
||||
public function getServers() {
|
||||
@@ -93,8 +146,9 @@ class ApiClient {
|
||||
return $this->request('DELETE', "/api/servers/$id");
|
||||
}
|
||||
|
||||
public function checkServerHealth($id) {
|
||||
return $this->request('POST', "/api/servers/$id/check");
|
||||
public function checkServerHealth($ids) {
|
||||
// Backend: POST /api/servers/check with body: [id1, id2, ...]
|
||||
return $this->request('POST', '/api/servers/check', $ids);
|
||||
}
|
||||
|
||||
// --- Push ---
|
||||
@@ -106,34 +160,30 @@ class ApiClient {
|
||||
]);
|
||||
}
|
||||
|
||||
// --- Logs ---
|
||||
// --- Sync Logs (per-server) ---
|
||||
|
||||
public function getAllLogs($limit = 100) {
|
||||
return $this->request('GET', "/api/servers/logs?limit=$limit");
|
||||
public function getServerLogs($serverId, $limit = 50) {
|
||||
return $this->request('GET', "/api/servers/$serverId/logs?limit=$limit");
|
||||
}
|
||||
|
||||
// --- Audit ---
|
||||
// --- Audit Logs ---
|
||||
|
||||
public function getAuditLogs($params = []) {
|
||||
$query = http_build_query($params);
|
||||
return $this->request('GET', "/api/servers/audit-logs?$query");
|
||||
return $this->request('GET', "/api/audit/?$query");
|
||||
}
|
||||
|
||||
// --- Dashboard ---
|
||||
|
||||
public function getDashboardStats() {
|
||||
// SQL COUNT — no need to fetch all 2000 servers
|
||||
$statsResp = $this->request('GET', '/api/servers/stats');
|
||||
$stats = $statsResp['data'] ?? [];
|
||||
$logsResp = $this->getAllLogs(10);
|
||||
$recentLogs = $logsResp['data'] ?? [];
|
||||
|
||||
return [
|
||||
'total_servers' => $stats['total_servers'] ?? 0,
|
||||
'online_servers' => $stats['online_servers'] ?? 0,
|
||||
'offline_servers' => $stats['offline_servers'] ?? 0,
|
||||
'push_ready' => $stats['push_ready'] ?? 0,
|
||||
'recent_logs' => is_array($recentLogs) ? $recentLogs : [],
|
||||
'total_servers' => $stats['total'] ?? 0,
|
||||
'online_servers' => $stats['online'] ?? 0,
|
||||
'offline_servers' => $stats['offline'] ?? 0,
|
||||
'categories' => $stats['categories'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+13
-33
@@ -123,39 +123,13 @@ try {
|
||||
$result = api()->getServer($id);
|
||||
break;
|
||||
case 'check_server':
|
||||
$id = $_GET['id'] ?? null;
|
||||
$result = api()->checkServerHealth($id);
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
$result = api()->checkServerHealth([$id]);
|
||||
break;
|
||||
case 'check_all':
|
||||
$servers = api()->getServers()['data'] ?? [];
|
||||
// Concurrent health checks using curl_multi
|
||||
$handles = [];
|
||||
$mh = curl_multi_init();
|
||||
foreach ($servers as $s) {
|
||||
$ch = curl_init(API_BASE . '/api/servers/' . $s['id'] . '/health');
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
|
||||
if (API_KEY) curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . API_KEY]);
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
$handles[$s['id']] = $ch;
|
||||
}
|
||||
// Execute all requests concurrently
|
||||
do { $status = curl_multi_exec($mh, $active); } while ($status === CURLM_CALL_MULTI_PERFORM);
|
||||
while ($active && $status === CURLM_OK) {
|
||||
curl_multi_select($mh, 1);
|
||||
do { $status = curl_multi_exec($mh, $active); } while ($status === CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
// Collect results
|
||||
$results = [];
|
||||
foreach ($servers as $s) {
|
||||
$ch = $handles[$s['id']];
|
||||
$resp = curl_multi_getcontent($ch);
|
||||
$data = json_decode($resp, true) ?: ['server_id' => $s['id'], 'is_online' => false];
|
||||
$results[] = $data;
|
||||
curl_multi_remove_handle($mh, $ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
$result = ['data' => $results, 'code' => 200];
|
||||
$serverIds = array_map(fn($s) => $s['id'], $servers);
|
||||
$result = api()->checkServerHealth($serverIds);
|
||||
break;
|
||||
|
||||
// --- Push ---
|
||||
@@ -167,7 +141,13 @@ try {
|
||||
// --- Logs ---
|
||||
case 'get_logs':
|
||||
$limit = (int)($_GET['limit'] ?? 100);
|
||||
$result = api()->getAllLogs($limit);
|
||||
$serverId = $_GET['server_id'] ?? null;
|
||||
if ($serverId) {
|
||||
$result = api()->getServerLogs(intval($serverId), $limit);
|
||||
} else {
|
||||
// Global logs endpoint
|
||||
$result = api()->request('GET', "/api/servers/logs?limit=$limit");
|
||||
}
|
||||
break;
|
||||
|
||||
// --- Dashboard ---
|
||||
@@ -263,8 +243,8 @@ try {
|
||||
$result = ['code' => 200, 'ip' => $ip];
|
||||
break;
|
||||
|
||||
case 'restart_multisync':
|
||||
exec('sudo systemctl restart multisync 2>&1', $out, $code);
|
||||
case 'restart_nexus':
|
||||
exec('sudo supervisorctl restart nexus 2>&1', $out, $code);
|
||||
$result = ['code' => $code === 0 ? 200 : 500, 'message' => $code === 0 ? 'Restarted' : implode("\n", $out)];
|
||||
break;
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ $resp = api()->getAuditLogs($params);
|
||||
$logs = $resp['data']['logs'] ?? [];
|
||||
$total = $resp['data']['total'] ?? 0;
|
||||
?>
|
||||
<?php $pageTitle = '审计日志 - MultiSync';
|
||||
<?php $pageTitle = '审计日志 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - 鉴权模块
|
||||
* Nexus - 鉴权模块
|
||||
* 所有需要登录的页面引入此文件即可
|
||||
*/
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - MySQL 数据库操作类
|
||||
* Nexus - MySQL 数据库操作类
|
||||
*
|
||||
* 注:主要 CRUD 已迁移到 Python API (server/api/servers.py)
|
||||
* 此文件仅保留 deploy.php 所需的 getServers() 和基本服务器操作
|
||||
@@ -131,7 +131,7 @@ class Database {
|
||||
function _encryptPw($plain) {
|
||||
if (empty($plain)) return $plain;
|
||||
if (!defined('API_KEY') || empty(API_KEY)) {
|
||||
error_log('MultiSync: SSH password not encrypted — API_KEY not configured');
|
||||
error_log('Nexus: SSH password not encrypted — API_KEY not configured');
|
||||
return $plain;
|
||||
}
|
||||
$key = hash('sha256', API_KEY, true);
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - 子服务器部署页面
|
||||
* Nexus - 子服务器部署页面
|
||||
* 根据当前网站地址自动生成安装命令
|
||||
*/
|
||||
require_once __DIR__ . '/auth.php';
|
||||
@@ -272,7 +272,7 @@ journalctl -u multisync -f <span class="code-comment"># 查看日志</span><
|
||||
</p>
|
||||
|
||||
<div class="code-block" id="cmdQuick">
|
||||
<button class="copy-btn" onclick="copyCode('cmdQuick', this)"><i class="fas fa-copy"></i> 复制</button><span class="code-comment"># 一键安装 MultiSync Agent</span>
|
||||
<button class="copy-btn" onclick="copyCode('cmdQuick', this)"><i class="fas fa-copy"></i> 复制</button><span class="code-comment"># 一键安装 Nexus Agent</span>
|
||||
curl -fsSL <span class="code-value"><?= htmlspecialchars($installScriptUrl) ?></span> -o /tmp/ms-install.sh && \
|
||||
bash /tmp/ms-install.sh \
|
||||
<span class="code-flag">--url</span> <span class="code-value"><?= htmlspecialchars($apiUrl) ?></span> \
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
$path = $_GET['path'] ?? '/www/wwwroot/';
|
||||
$pageTitle = '文件管理 - MultiSync';
|
||||
$pageTitle = '文件管理 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
?>
|
||||
<style>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
$pageTitle = '文件管理 - MultiSync';
|
||||
$pageTitle = '文件管理 - Nexus';
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync — 首次使用引导(3 步)
|
||||
* Nexus — 首次使用引导(3 步)
|
||||
*/
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/api_client.php';
|
||||
@@ -18,7 +18,7 @@ $recentLogs = $logResp['data'] ?? [];
|
||||
$hasPushed = !empty($recentLogs);
|
||||
|
||||
$step = $hasPushed ? 3 : ($hasOnline ? 2 : ($hasServers ? 1 : 0));
|
||||
$pageTitle = '使用引导 - MultiSync';
|
||||
$pageTitle = '使用引导 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
|
||||
+10
-2
@@ -5,7 +5,7 @@ require_once __DIR__ . '/api_client.php';
|
||||
$stats = api()->getDashboardStats();
|
||||
$logResp = api()->getAllLogs(10);
|
||||
$recentLogs = $logResp['data'] ?? [];
|
||||
$pageTitle = '仪表盘 - MultiSync';
|
||||
$pageTitle = '仪表盘 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
|
||||
@@ -118,11 +118,19 @@ $pyOnline = $_SESSION['api_online'] ?? false;
|
||||
<script>
|
||||
const API_BASE = <?= json_encode(defined('API_BASE_URL') ? API_BASE_URL : 'http://localhost:8600') ?>;
|
||||
const API_KEY = <?= json_encode(get_api_key()) ?>;
|
||||
const JWT_TOKEN = <?= json_encode($_SESSION['access_token'] ?? '') ?>;
|
||||
|
||||
function apiHeaders() {
|
||||
const h = {'Content-Type': 'application/json'};
|
||||
if (JWT_TOKEN) h['Authorization'] = 'Bearer ' + JWT_TOKEN;
|
||||
else if (API_KEY) h['X-API-Key'] = API_KEY;
|
||||
return h;
|
||||
}
|
||||
|
||||
// === Push Chart ===
|
||||
let pushChart = null;
|
||||
function loadPushStats() {
|
||||
fetch(API_BASE + '/api/servers/logs?limit=500', { headers: API_KEY ? {'X-API-Key': API_KEY} : {} })
|
||||
fetch(API_BASE + '/api/servers/logs?limit=500', { headers: apiHeaders() })
|
||||
.then(r => r.json()).then(data => {
|
||||
const logs = data.data || data || [];
|
||||
const cutoff = Date.now() - 7*86400000;
|
||||
|
||||
+74
-15
@@ -71,8 +71,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||
);
|
||||
|
||||
// ── Create tables (match SQLAlchemy models exactly) ──
|
||||
// ── Create tables in dependency order (referenced tables first) ──
|
||||
$tables = [
|
||||
"platforms" => "
|
||||
CREATE TABLE IF NOT EXISTS platforms (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE COMMENT '平台名称: Linux服务器/Windows/MySQL',
|
||||
category VARCHAR(50) NOT NULL COMMENT '分类: host/database/device',
|
||||
type VARCHAR(50) NOT NULL COMMENT '类型: linux/windows/mysql/switch',
|
||||
default_protocols JSON COMMENT '默认协议: [{name:\"ssh\",port:22,primary:true}]',
|
||||
charset VARCHAR(20) DEFAULT 'utf-8' COMMENT '默认字符集',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"nodes" => "
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL COMMENT '节点名称',
|
||||
parent_id INT COMMENT '父节点ID',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序权重',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES nodes(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"admins" => "
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
totp_secret VARCHAR(64),
|
||||
totp_enabled TINYINT(1) DEFAULT 0,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
jwt_refresh_token VARCHAR(500) COMMENT 'JWT刷新令牌',
|
||||
jwt_token_expires DATETIME COMMENT '令牌过期时间',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"servers" => "
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -90,13 +123,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
agent_api_key VARCHAR(255) COMMENT 'Agent API密钥',
|
||||
description TEXT COMMENT '备注说明',
|
||||
target_path VARCHAR(500) COMMENT '推送目标路径',
|
||||
category VARCHAR(100) COMMENT '服务器分类',
|
||||
category VARCHAR(100) COMMENT '服务器分类(旧字段,保留兼容)',
|
||||
platform_id INT COMMENT '平台类型ID',
|
||||
node_id INT COMMENT '所属节点ID',
|
||||
protocols JSON COMMENT '实例级协议覆盖',
|
||||
extra_attrs JSON COMMENT '扩展属性',
|
||||
connectivity VARCHAR(20) DEFAULT 'unknown' COMMENT '连接状态: ok/err/unknown',
|
||||
is_online TINYINT(1) DEFAULT 0 COMMENT '是否在线',
|
||||
last_heartbeat DATETIME COMMENT '最后心跳时间',
|
||||
system_info TEXT COMMENT '系统信息JSON(Agent上报)',
|
||||
agent_version VARCHAR(20) COMMENT 'Agent版本',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (platform_id) REFERENCES platforms(id),
|
||||
FOREIGN KEY (node_id) REFERENCES nodes(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"sync_logs" => "
|
||||
CREATE TABLE IF NOT EXISTS sync_logs (
|
||||
@@ -119,18 +159,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
finished_at DATETIME,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"admins" => "
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
totp_secret VARCHAR(64),
|
||||
totp_enabled TINYINT(1) DEFAULT 0,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"login_attempts" => "
|
||||
CREATE TABLE IF NOT EXISTS login_attempts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -227,6 +255,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
database VARCHAR(100) COMMENT '数据库库名',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"ssh_sessions" => "
|
||||
CREATE TABLE IF NOT EXISTS ssh_sessions (
|
||||
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID',
|
||||
server_id INT NOT NULL COMMENT '服务器ID',
|
||||
admin_id INT COMMENT '操作人ID',
|
||||
remote_addr VARCHAR(45) COMMENT '客户端IP',
|
||||
status VARCHAR(20) DEFAULT 'active' COMMENT 'active/closed',
|
||||
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
closed_at DATETIME,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"command_logs" => "
|
||||
CREATE TABLE IF NOT EXISTS command_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(36) COMMENT 'SSH会话ID',
|
||||
server_id INT NOT NULL COMMENT '服务器ID',
|
||||
admin_id INT COMMENT '操作人ID',
|
||||
command VARCHAR(2000) NOT NULL COMMENT '执行的命令',
|
||||
remote_addr VARCHAR(45) COMMENT '客户端IP',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (session_id) REFERENCES ssh_sessions(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
];
|
||||
|
||||
foreach ($tables as $name => $sql) {
|
||||
@@ -237,8 +290,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$indexes = [
|
||||
"ALTER TABLE servers ADD INDEX idx_servers_is_online (is_online)",
|
||||
"ALTER TABLE servers ADD INDEX idx_servers_category (category)",
|
||||
"ALTER TABLE servers ADD INDEX idx_servers_platform_id (platform_id)",
|
||||
"ALTER TABLE servers ADD INDEX idx_servers_node_id (node_id)",
|
||||
"ALTER TABLE sync_logs ADD INDEX idx_sync_logs_srv_start (server_id, started_at)",
|
||||
"ALTER TABLE login_attempts ADD INDEX idx_login_attempts_username_ip (username, ip_address)",
|
||||
"ALTER TABLE ssh_sessions ADD INDEX idx_ssh_sessions_server_id (server_id)",
|
||||
"ALTER TABLE ssh_sessions ADD INDEX idx_ssh_sessions_admin_id (admin_id)",
|
||||
"ALTER TABLE command_logs ADD INDEX idx_cmdlog_srv_time (server_id, created_at)",
|
||||
"ALTER TABLE command_logs ADD INDEX idx_cmdlog_session_id (session_id)",
|
||||
];
|
||||
foreach ($indexes as $sql) {
|
||||
try { $pdo->exec($sql); } catch (PDOException $e) {
|
||||
|
||||
+27
-8
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - 登录页面
|
||||
* Nexus - 登录页面
|
||||
* 支持: 登录失败次数限制(5次冻结) + Google 身份验证器(TOTP)
|
||||
*/
|
||||
session_start();
|
||||
@@ -169,14 +169,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-API-Key: ' . $apiKey],
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode(['username' => $username, 'password' => $password]),
|
||||
]);
|
||||
$apiResp = json_decode(curl_exec($ch), true);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if (($apiResp['status'] ?? '') === 'ok' || ($apiResp['status'] ?? '') === 'totp_required') {
|
||||
|
||||
// FastAPI JWT response: {"success":true, "access_token":"...", ...}
|
||||
// TOTP required: HTTP 202 with detail message
|
||||
if ($httpCode === 202 || ($apiResp['reason'] ?? '') === 'totp_required') {
|
||||
$passwordValid = true;
|
||||
$admin = ['username' => $username, 'id' => $apiResp['admin_id'] ?? null];
|
||||
$needTotp = true;
|
||||
$tempUser = $username;
|
||||
$_SESSION['totp_pending'] = $username;
|
||||
$_SESSION['auth_password'] = $password;
|
||||
$_SESSION['auth_time'] = time();
|
||||
} elseif (($apiResp['success'] ?? false) === true && isset($apiResp['access_token'])) {
|
||||
$passwordValid = true;
|
||||
$admin = [
|
||||
'username' => $username,
|
||||
'id' => $apiResp['admin']['id'] ?? null,
|
||||
];
|
||||
// Store JWT tokens in session for API calls
|
||||
$_SESSION['access_token'] = $apiResp['access_token'];
|
||||
$_SESSION['refresh_token'] = $apiResp['refresh_token'] ?? '';
|
||||
$_SESSION['token_expires'] = time() + ($apiResp['expires_in'] ?? 1800);
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
@@ -269,7 +288,7 @@ $expiredMsg = ($msg === 'expired') ? '会话已过期,请重新登录' : '';
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - MultiSync</title>
|
||||
<title>登录 - Nexus</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -495,9 +514,9 @@ $expiredMsg = ($msg === 'expired') ? '会话已过期,请重新登录' : '';
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<div class="logo">MS</div>
|
||||
<h1>MultiSync</h1>
|
||||
<p>多服务器文件同步管理系统</p>
|
||||
<div class="logo">N</div>
|
||||
<h1>Nexus</h1>
|
||||
<p>服务器运维管理平台</p>
|
||||
</div>
|
||||
|
||||
<div class="login-card">
|
||||
@@ -566,7 +585,7 @@ $expiredMsg = ($msg === 'expired') ? '会话已过期,请重新登录' : '';
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
MultiSync v1.0.0 |
|
||||
Nexus v6.0.0 |
|
||||
<a href="install.php">运行安装向导</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+22
-1
@@ -1,8 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - 退出登录
|
||||
* Nexus - 退出登录
|
||||
*/
|
||||
session_start();
|
||||
|
||||
// 通知后端注销 JWT refresh token
|
||||
if (!empty($_SESSION['refresh_token'])) {
|
||||
try {
|
||||
$apiUrl = defined('API_BASE_URL') ? API_BASE_URL : 'http://localhost:8600';
|
||||
$ch = curl_init("$apiUrl/api/auth/logout");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_TIMEOUT => 3,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . ($_SESSION['access_token'] ?? ''),
|
||||
],
|
||||
CURLOPT_POSTFIELDS => json_encode(['admin_id' => $_SESSION['admin_id'] ?? 0]),
|
||||
]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
session_unset();
|
||||
session_destroy();
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ $logs = $logResp['data'] ?? [];
|
||||
$serverResp = api()->getServers();
|
||||
$servers = $serverResp['data'] ?? [];
|
||||
?>
|
||||
<?php $pageTitle = '同步日志 - MultiSync';
|
||||
<?php $pageTitle = '同步日志 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ $totalPages = max(1, (int)ceil($total / $perPage));
|
||||
// 状态映射(中文显示)
|
||||
$statusLabels = ['pending' => '等待重试', 'retrying' => '重试中', 'abandoned' => '已放弃'];
|
||||
$statusBadges = ['pending' => 'badge-info', 'retrying' => 'badge-warning', 'abandoned' => 'badge-danger'];
|
||||
$pageTitle = '重试队列 - MultiSync';
|
||||
$pageTitle = '重试队列 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ $schedules = $resp['data'] ?? [];
|
||||
$srvResp = api()->getServers();
|
||||
$servers = $srvResp['data'] ?? [];
|
||||
|
||||
$pageTitle = '定时推送 - MultiSync';
|
||||
$pageTitle = '定时推送 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ if ($action === 'list') {
|
||||
$server = $resp['data'] ?? null;
|
||||
}
|
||||
|
||||
$pageTitle = '服务器管理 - MultiSync';
|
||||
$pageTitle = '服务器管理 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - 系统设置(含谷歌身份验证器绑定)
|
||||
* Nexus - 系统设置(含谷歌身份验证器绑定)
|
||||
*/
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/db.php';
|
||||
@@ -125,7 +125,7 @@ $setupSecret = $_SESSION['totp_setup_secret'] ?? '';
|
||||
$setupMode = (!empty($setupSecret)) || (isset($_GET['setup']) && !empty($_SESSION['totp_setup_secret']));
|
||||
|
||||
$totpEnabled = !empty($admin['totp_enabled']);
|
||||
$pageTitle = '系统设置 - MultiSync';
|
||||
$pageTitle = '系统设置 - Nexus';
|
||||
require_once __DIR__ . '/_layout_head.php';
|
||||
require_once __DIR__ . '/_sidebar_adminlte.php';
|
||||
?>
|
||||
@@ -917,7 +917,7 @@ loadSessionLifetime();
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
<script>
|
||||
new QRCode(document.getElementById("qrcode"), {
|
||||
text: <?= json_encode(TOTP::getQRCodeUri($setupSecret, CURRENT_USER, 'MultiSync')) ?>,
|
||||
text: <?= json_encode(TOTP::getQRCodeUri($setupSecret, CURRENT_USER, 'Nexus')) ?>,
|
||||
width: 200, height: 200,
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,15 +1,15 @@
|
||||
<?php
|
||||
// MultiSync File Manager — powered by Tiny File Manager v2.6
|
||||
// Nexus File Manager — powered by Tiny File Manager v2.6
|
||||
// Default Configuration
|
||||
$CONFIG = '{"lang":"zh","error_reporting":false,"show_hidden":false,"hide_Cols":false,"theme":"light"}';
|
||||
|
||||
define('VERSION', '2.6');
|
||||
define('APP_TITLE', 'MultiSync 文件管理');
|
||||
define('APP_TITLE', 'Nexus 文件管理');
|
||||
|
||||
// --- MultiSync 集成 ---
|
||||
// --- Nexus 集成 ---
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
// Auth via MultiSync auth.php(已有 session 保护)
|
||||
// Auth via Nexus auth.php(已有 session 保护)
|
||||
$use_auth = false; // 关闭内置 auth,由 auth.php 接管
|
||||
$auth_users = array();
|
||||
$readonly_users = array();
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* MultiSync - TOTP (Google Authenticator) 纯 PHP 实现
|
||||
* Nexus - TOTP (Google Authenticator) 纯 PHP 实现
|
||||
* RFC 6238 compliant
|
||||
*/
|
||||
|
||||
@@ -54,7 +54,7 @@ class TOTP {
|
||||
/**
|
||||
* 生成 Google Authenticator URI(用于生成二维码)
|
||||
*/
|
||||
public static function getQRCodeUri($secret, $email, $issuer = 'MultiSync') {
|
||||
public static function getQRCodeUri($secret, $email, $issuer = 'Nexus') {
|
||||
$issuer = rawurlencode($issuer);
|
||||
$email = rawurlencode($email);
|
||||
return "otpauth://totp/{$issuer}:{$email}?secret={$secret}&issuer={$issuer}&digits=6&period=30";
|
||||
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
// MultiSync
|
||||
// Nexus
|
||||
require_once __DIR__ . "/auth.php";
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
include __DIR__ . '/config.php';}
|
||||
if (!defined('APP_TITLE')) define('APP_TITLE', '文件管理器');
|
||||
if (!isset($base_url)) $base_url = '';
|
||||
if (!isset($auth_user)) $auth_user = 'admin'; // MultiSync: 不生效,auth.php 已接管鉴权
|
||||
if (!isset($auth_pass)) $auth_pass = '$2y$10$m7JT3rdrZ3nK4tKxpJ8z5.pDalU75BZH9nu82zBJkVNhLJgoC6uXm'; // MultiSync: 不生效
|
||||
if (!isset($auth_user)) $auth_user = 'admin'; // Nexus: 不生效,auth.php 已接管鉴权
|
||||
if (!isset($auth_pass)) $auth_pass = '$2y$10$m7JT3rdrZ3nK4tKxpJ8z5.pDalU75BZH9nu82zBJkVNhLJgoC6uXm'; // Nexus: 不生效
|
||||
if (!isset($root_path)) $root_path = $_SERVER['DOCUMENT_ROOT'];
|
||||
$_doc_root = rtrim(str_replace('\\', '/', realpath($_SERVER['DOCUMENT_ROOT'])), '/');
|
||||
$_root_real = rtrim(str_replace('\\', '/', realpath($root_path)), '/');
|
||||
@@ -368,8 +368,8 @@ if ($a !== $b && strpos($a . '/', $b . '/') !== 0) {
|
||||
http_response_code(400);
|
||||
die('操作无效');}}
|
||||
$self = 'http' . (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 's' : '') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
|
||||
$is_logged = !empty($_SESSION['logged_in']); // MultiSync: auth.php 已接管鉴权
|
||||
if (isset($_GET['logout'])) { session_destroy(); header('Location: login.php'); exit; } // MultiSync: 直接跳 login.php
|
||||
$is_logged = !empty($_SESSION['logged_in']); // Nexus: auth.php 已接管鉴权
|
||||
if (isset($_GET['logout'])) { session_destroy(); header('Location: login.php'); exit; } // Nexus: 直接跳 login.php
|
||||
if ($is_logged) fm_trash_clean(7);
|
||||
if ($is_logged && isset($_POST['ajax_star'])) {
|
||||
header('Content-Type: application/json');
|
||||
@@ -806,7 +806,7 @@ exit;}
|
||||
if (isset($_POST['ajax_logout'])) {
|
||||
session_destroy();
|
||||
ob_end_clean();
|
||||
echo json_encode(['success' => true, 'redirect' => 'login.php']); // MultiSync: logout 跳 login.php
|
||||
echo json_encode(['success' => true, 'redirect' => 'login.php']); // Nexus: logout 跳 login.php
|
||||
exit;}
|
||||
ob_end_clean();}
|
||||
if ($is_logged && isset($_GET['ajax_trash_list'])) {
|
||||
@@ -1062,7 +1062,7 @@ if(typeof closeDesktopMenu==='function')closeDesktopMenu();}
|
||||
<script src="ace/ace.js"></script> <!-- 先尝试 CDN.js失败时自动回退到本地 /js/ace.js-->
|
||||
</head>
|
||||
<body class="<?=$is_logged?'logged':''?>">
|
||||
<?php if (!$is_logged): header('Location: login.php'); exit; // MultiSync 兜底:auth.php 已拦截,此处防止边缘情况 ?>
|
||||
<?php if (!$is_logged): header('Location: login.php'); exit; // Nexus 兜底:auth.php 已拦截,此处防止边缘情况 ?>
|
||||
<?php else: ?>
|
||||
<!-- 主界面 -->
|
||||
<div class="music-panel" id="musicPanel">
|
||||
|
||||
Reference in New Issue
Block a user