Files
p2ns/includes/admin/admin-backend/routes/static.js
T
2026-05-30 21:01:31 -04:00

117 lines
3.8 KiB
JavaScript

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|branding\/[\w.-]+\.png|[\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', '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 {
await serveFile(pathModule.join(ADMIN_FRONTEND_DIR, 'index.html'), res);
} catch (err) {
logError('Admin', `Failed to serve index.html: ${err.message}`);
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 {
await serveFile(TAILWIND_CSS, res);
} catch (err) {
logError('Admin', `Failed to serve tailwind.css: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Failed to load Tailwind CSS');
}
return true;
}
if (method === 'GET' && (urlPath === '/favicon.ico' || urlPath === '/apple-touch-icon.png')) {
const faviconFile = urlPath === '/favicon.ico'
? pathModule.join(ADMIN_FRONTEND_DIR, 'favicon.ico')
: pathModule.join(ADMIN_FRONTEND_DIR, 'branding', 'p2ns-favicon-192.png');
try {
await serveFile(faviconFile, res);
} catch (err) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not Found');
}
return true;
}
if (method === 'GET') {
const served = await serveAdminFrontendAsset(urlPath, res);
if (served) {
return true;
}
}
return false;
}
module.exports = { handleStaticRoutes };