forked from snxraven/p2ns
Full Redesign of p2ns.admin
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
const { logError } = require('../../../infrastructure/logger');
|
||||
const { collectAdminAlerts } = require('../../alerts-collector');
|
||||
|
||||
async function handleAlertsRoutes(req, res) {
|
||||
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
||||
const method = req.method;
|
||||
|
||||
if (method === 'GET' && urlPath === '/api/admin/alerts') {
|
||||
try {
|
||||
const payload = await collectAdminAlerts();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(payload));
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to collect admin alerts: ${err.message}`);
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ error: 'Failed to collect admin alerts' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { handleAlertsRoutes };
|
||||
@@ -7,7 +7,7 @@ const { validateHolesailClient } = require('../../../infrastructure/validation')
|
||||
const { createInterfaceForDomain } = require('../../../networking/virtual_interfaces');
|
||||
const { logDebug, logError, logInfo, logWarn } = require('../../../infrastructure/logger');
|
||||
const { startHolesailServer, saveHolesailServers } = require('../holesail-servers');
|
||||
const { startForkedHolesailClient, saveHolesailClients } = require('../holesail-clients');
|
||||
const { startForkedHolesailClient, saveHolesailClients, getHolesailClientStatus } = require('../holesail-clients');
|
||||
const { ensurePortFree } = require('../port-management');
|
||||
const { broadcast } = require('../websocket');
|
||||
const { getConsensusState, getClaimClients, updateClaimClients } = require('../../../core/core');
|
||||
@@ -42,19 +42,8 @@ async function handleHolesailRoutes(req, res) {
|
||||
if (method === 'GET' && urlPath === '/api/holesail-clients') {
|
||||
try {
|
||||
const clients = Array.from(state.holesailClientOpts.entries()).map(([id, opts]) => {
|
||||
const child = state.holesailClientChildren.get(id);
|
||||
const info = state.holesailClientInfos.get(id) || {};
|
||||
const key = `${opts.domain}:${opts.port}`;
|
||||
const isHolesailActive = state.holesails.has(key);
|
||||
const isChildRunning = child && !child.killed;
|
||||
let status = 'stopped';
|
||||
if (isChildRunning && isHolesailActive && info.state !== 'error') {
|
||||
status = 'running';
|
||||
} else if (isChildRunning || isHolesailActive) {
|
||||
status = 'starting';
|
||||
} else if (info.state === 'error') {
|
||||
status = 'error';
|
||||
}
|
||||
const status = getHolesailClientStatus(id, opts, info);
|
||||
return { id, opts, info: { ...info, state: status } };
|
||||
});
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
||||
@@ -16,6 +16,7 @@ const { handleDiagnosticsRoutes } = require('./diagnostics');
|
||||
const { handleConsensusRoutes } = require('./consensus');
|
||||
const { handlePluginsRoutes } = require('./plugins');
|
||||
const { handleLogsRoutes } = require('./logs');
|
||||
const { handleAlertsRoutes } = require('./alerts');
|
||||
|
||||
async function handleAdminRequest(req, res) {
|
||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||
@@ -54,9 +55,10 @@ async function handleAdminRequest(req, res) {
|
||||
if (await handleConsensusRoutes(req, res)) return;
|
||||
if (await handlePluginsRoutes(req, res)) return;
|
||||
if (await handleLogsRoutes(req, res)) return;
|
||||
if (await handleAlertsRoutes(req, res)) return;
|
||||
|
||||
// No route matched
|
||||
res.writeHead(404);
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Not Found');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
const state = require('../../../infrastructure/state');
|
||||
const { cleanupInterfaces } = require('../../../maintenance/cleanup');
|
||||
const { logError } = require('../../../infrastructure/logger');
|
||||
const { broadcast } = require('../websocket');
|
||||
const { scheduleInterfacesBroadcast } = require('../interfaces-broadcast');
|
||||
const { buildInterfacesResponse, removeOrphanedInterfaceIp } = require('../../interfaces-data');
|
||||
|
||||
function readJsonBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch (err) {
|
||||
reject(new Error('Invalid JSON body'));
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleInterfacesRoutes(req, res) {
|
||||
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
||||
@@ -9,9 +24,9 @@ async function handleInterfacesRoutes(req, res) {
|
||||
|
||||
if (method === 'GET' && urlPath === '/api/interfaces') {
|
||||
try {
|
||||
const interfaces = Array.from(state.domainToIPMap.entries()).map(([domain, ip]) => ({ domain, ip }));
|
||||
const payload = await buildInterfacesResponse();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(interfaces));
|
||||
res.end(JSON.stringify(payload));
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to fetch interfaces: ${err.message}`);
|
||||
res.writeHead(500);
|
||||
@@ -20,10 +35,25 @@ async function handleInterfacesRoutes(req, res) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method === 'POST' && urlPath === '/api/interfaces/remove-ip') {
|
||||
try {
|
||||
const { ip } = await readJsonBody(req);
|
||||
const removedIp = await removeOrphanedInterfaceIp(ip);
|
||||
scheduleInterfacesBroadcast();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, ip: removedIp }));
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to remove orphaned IP: ${err.message}`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method === 'POST' && urlPath === '/api/cleanup-interfaces') {
|
||||
try {
|
||||
await cleanupInterfaces();
|
||||
broadcast({ type: 'update-interfaces' });
|
||||
scheduleInterfacesBroadcast();
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('OK');
|
||||
} catch (err) {
|
||||
@@ -38,4 +68,3 @@ async function handleInterfacesRoutes(req, res) {
|
||||
}
|
||||
|
||||
module.exports = { handleInterfacesRoutes };
|
||||
|
||||
|
||||
@@ -2,95 +2,112 @@ const fs = require('fs').promises;
|
||||
const pathModule = require('path');
|
||||
const { logDebug, logError } = require('../../../infrastructure/logger');
|
||||
|
||||
const ADMIN_FRONTEND_DIR = pathModule.join(__dirname, '..', '..', 'admin-frontend');
|
||||
const TAILWIND_CSS = pathModule.join(__dirname, '..', '..', '..', 'css', 'tailwind.css');
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.png': 'image/png',
|
||||
'.json': 'application/json',
|
||||
'.webmanifest': 'application/manifest+json'
|
||||
};
|
||||
|
||||
function isPathInsideDir(candidate, baseDir) {
|
||||
const resolvedBase = pathModule.resolve(baseDir);
|
||||
const resolvedCandidate = pathModule.resolve(candidate);
|
||||
return resolvedCandidate === resolvedBase
|
||||
|| resolvedCandidate.startsWith(resolvedBase + pathModule.sep);
|
||||
}
|
||||
|
||||
async function serveFile(filePath, res) {
|
||||
const ext = pathModule.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
const body = await fs.readFile(filePath);
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
async function serveAdminFrontendAsset(urlPath, res) {
|
||||
if (!/^\/(?:ui\/[\w.-]+\.js|[\w.-]+\.(?:css|js|svg|ico|png|json|webmanifest))$/.test(urlPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relativePath = urlPath.replace(/^\//, '');
|
||||
const filePath = pathModule.join(ADMIN_FRONTEND_DIR, relativePath);
|
||||
if (!isPathInsideDir(filePath, ADMIN_FRONTEND_DIR)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await serveFile(filePath, res);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
logError('Admin', `Failed to serve ${urlPath}: ${err.message}`);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Failed to load asset');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStaticRoutes(req, res) {
|
||||
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
||||
const method = req.method;
|
||||
|
||||
const tabs = ['domains', 'host', 'local-dns', 'entries', 'peers', 'certs', 'interfaces', 'logs', 'settings', 'stats'];
|
||||
const tabs = ['domains', 'host', 'local-dns', 'entries', 'peers', 'certs', 'interfaces', 'logs', 'settings', 'stats', 'backups', 'plugins'];
|
||||
if (method === 'GET' && urlPath.startsWith('/') && tabs.includes(urlPath.substring(1))) {
|
||||
res.writeHead(302, { 'Location': `/#${urlPath.substring(1)}` });
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (method === 'GET' && urlPath === '/') {
|
||||
logDebug('Admin', 'Serving admin panel HTML');
|
||||
try {
|
||||
const html = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', 'index.html'), 'utf8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(html);
|
||||
await serveFile(pathModule.join(ADMIN_FRONTEND_DIR, 'index.html'), res);
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to serve index.html: ${err.message}`);
|
||||
res.writeHead(500);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Failed to load admin panel');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (method === 'GET' && urlPath === '/tailwind.css') {
|
||||
try {
|
||||
const css = await fs.readFile(pathModule.join(__dirname, '..', '..', '..', 'css', 'tailwind.css'), 'utf8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/css' });
|
||||
res.end(css);
|
||||
await serveFile(TAILWIND_CSS, res);
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to serve tailwind.css: ${err.message}`);
|
||||
res.writeHead(500);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Failed to load Tailwind CSS');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method === 'GET' && urlPath === '/styles.css') {
|
||||
try {
|
||||
const css = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', 'styles.css'), 'utf8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/css' });
|
||||
res.end(css);
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to serve styles.css: ${err.message}`);
|
||||
res.writeHead(500);
|
||||
res.end('Failed to load styles');
|
||||
|
||||
if (method === 'GET') {
|
||||
const served = await serveAdminFrontendAsset(urlPath, res);
|
||||
if (served) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Serve frontend JavaScript files
|
||||
if (method === 'GET' && (urlPath === '/admin.js' || urlPath === '/utils.js' || urlPath === '/ws-client.js')) {
|
||||
|
||||
if (method === 'GET' && urlPath === '/favicon.ico') {
|
||||
try {
|
||||
const fileName = urlPath.substring(1); // Remove leading '/'
|
||||
const js = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', fileName), 'utf8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/javascript' });
|
||||
res.end(js);
|
||||
await serveFile(pathModule.join(ADMIN_FRONTEND_DIR, 'favicon.svg'), res);
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to serve ${urlPath}: ${err.message}`);
|
||||
res.writeHead(500);
|
||||
res.end('Failed to load script');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Serve UI module files
|
||||
if (method === 'GET' && urlPath.startsWith('/ui/') && urlPath.endsWith('.js')) {
|
||||
try {
|
||||
const fileName = urlPath.substring(1); // Remove leading '/' -> 'ui/filename.js'
|
||||
const js = await fs.readFile(pathModule.join(__dirname, '..', '..', 'admin-frontend', fileName), 'utf8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/javascript' });
|
||||
res.end(js);
|
||||
} catch (err) {
|
||||
logError('Admin', `Failed to serve ${urlPath}: ${err.message}`);
|
||||
res.writeHead(404);
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Not Found');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (urlPath === '/favicon.ico') {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { handleStaticRoutes };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user