refactor: extract static/search/feedback route handlers
server/routes/static.js (/, /index.html, /index-editorial.html, /favicon.ico, /lia), server/routes/search.js (/api/search, /api/visitors), server/routes/feedback.js (/api/feedback OPTIONS/POST/GET). Verified via node --check, eslint no-undef (only the 3 known pre-existing bugs remain, no new ones), and smoke-test harness (28/28 match). server.js: 3714 -> 3556 lines.
This commit is contained in:
parent
a80229a9a5
commit
3399392f1f
199
server.js
199
server.js
@ -36,9 +36,6 @@ const {
|
|||||||
fetchPeeringDBWithRetry,
|
fetchPeeringDBWithRetry,
|
||||||
} = require("./server/services/peeringdb");
|
} = require("./server/services/peeringdb");
|
||||||
|
|
||||||
const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env";
|
|
||||||
const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json";
|
|
||||||
|
|
||||||
// ── SMTP / Email ──────────────────────────────────────────────
|
// ── SMTP / Email ──────────────────────────────────────────────
|
||||||
const { sendFeedbackMail } = require('./src/backend/services/smtp');
|
const { sendFeedbackMail } = require('./src/backend/services/smtp');
|
||||||
// ── SMTP / Email ──────────────────────────────────────────────
|
// ── SMTP / Email ──────────────────────────────────────────────
|
||||||
@ -220,6 +217,10 @@ function proxyToApiServer(req, res, targetUrl) {
|
|||||||
// HTTP Server
|
// HTTP Server
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
const staticRoutes = require("./server/routes/static");
|
||||||
|
const searchRoutes = require("./server/routes/search");
|
||||||
|
const feedbackRoutes = require("./server/routes/feedback");
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||||
@ -243,211 +244,51 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const host = (req.headers.host || '').split(':')[0];
|
const host = (req.headers.host || '').split(':')[0];
|
||||||
|
|
||||||
if (reqPath === "/" || reqPath === "/index.html") {
|
if (reqPath === "/" || reqPath === "/index.html") {
|
||||||
// shell.peercortex.org → admin feedback terminal (check first)
|
return staticRoutes.handleRoot(req, res, host, reqPath);
|
||||||
if (host === 'shell.peercortex.org') {
|
|
||||||
try {
|
|
||||||
const html = fs.readFileSync('/opt/peercortex-app/public/shell.html', 'utf8');
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
return res.end(html);
|
|
||||||
} catch (_e) {
|
|
||||||
res.writeHead(500);
|
|
||||||
return res.end('shell.html not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// v2.peercortex.org → redirect to main domain
|
|
||||||
if (host === 'v2.peercortex.org') {
|
|
||||||
res.writeHead(301, { Location: 'https://peercortex.org' + reqPath });
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
const htmlFile = "index.html";
|
|
||||||
try {
|
|
||||||
const html = fs.readFileSync("/opt/peercortex-app/public/" + htmlFile, "utf8");
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
return res.end(html);
|
|
||||||
} catch (_e) {
|
|
||||||
res.writeHead(500);
|
|
||||||
return res.end(htmlFile + " not found");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct access to editorial version
|
// Direct access to editorial version
|
||||||
if (reqPath === "/index-editorial.html") {
|
if (reqPath === "/index-editorial.html") {
|
||||||
try {
|
return staticRoutes.handleIndexEditorial(req, res);
|
||||||
const html = fs.readFileSync("/opt/peercortex-app/public/index-editorial.html", "utf8");
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
return res.end(html);
|
|
||||||
} catch (_e) {
|
|
||||||
res.writeHead(500);
|
|
||||||
return res.end("index-editorial.html not found");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// Name Search (RIPE Stat + PeeringDB combined)
|
||||||
// Feedback API
|
if (reqPath === "/api/search") {
|
||||||
// ============================================================
|
return searchRoutes.handleSearch(req, res);
|
||||||
|
|
||||||
|
|
||||||
// ── Name Search (RIPE Stat + PeeringDB combined) ─────────────
|
|
||||||
if (reqPath === '/api/search') {
|
|
||||||
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
||||||
const q = (params.get('q') || '').trim();
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
res.setHeader('Cache-Control', 'public, max-age=120');
|
|
||||||
if (!q || q.length < 2) { res.writeHead(400); return res.end(JSON.stringify({error:'query too short'})); }
|
|
||||||
try {
|
|
||||||
const results = [];
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
// Source 1: RIPE Stat searchcomplete (fast, covers ASNs + org names)
|
|
||||||
try {
|
|
||||||
const ripeUrl = 'https://stat.ripe.net/data/searchcomplete/data.json?resource=' + encodeURIComponent(q);
|
|
||||||
const ripeData = await fetchJSONWithRetry(ripeUrl, { timeout: 6000 });
|
|
||||||
const cats = ripeData && ripeData.data && ripeData.data.categories || [];
|
|
||||||
for (var ci = 0; ci < cats.length; ci++) {
|
|
||||||
var suggs = cats[ci].suggestions || [];
|
|
||||||
for (var si = 0; si < suggs.length; si++) {
|
|
||||||
var s = suggs[si];
|
|
||||||
var val = (s.value || '').toString();
|
|
||||||
// Only ASN results
|
|
||||||
if (/^AS\d+$/i.test(val) && !seen.has(val)) {
|
|
||||||
seen.add(val);
|
|
||||||
// Use description (e.g. "FLEXOPTIX, DE") as the display label
|
|
||||||
var ripeName = s.description || s.label || val;
|
|
||||||
results.push({ asn: val.replace(/^AS/i,''), label: ripeName, description: '', source: 'RIPE Stat' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) { /* RIPE Stat failed, continue */ }
|
|
||||||
|
|
||||||
// Source 2: PeeringDB name search (best for network operator names)
|
|
||||||
try {
|
|
||||||
var pdbUrl = 'https://www.peeringdb.com/api/net?name__icontains=' + encodeURIComponent(q) + '&depth=1&limit=10';
|
|
||||||
if (PEERINGDB_API_KEY) pdbUrl += '&key=' + PEERINGDB_API_KEY;
|
|
||||||
const pdbData = await fetchJSONWithRetry(pdbUrl, { timeout: 8000 });
|
|
||||||
var nets = pdbData && pdbData.data || [];
|
|
||||||
for (var ni = 0; ni < nets.length; ni++) {
|
|
||||||
var net = nets[ni];
|
|
||||||
var asnKey = 'AS' + net.asn;
|
|
||||||
if (net.asn && !seen.has(asnKey)) {
|
|
||||||
seen.add(asnKey);
|
|
||||||
var pdbDesc = [net.info_type, net.country].filter(Boolean).join(' · ');
|
|
||||||
results.push({
|
|
||||||
asn: String(net.asn),
|
|
||||||
label: net.name || asnKey,
|
|
||||||
description: pdbDesc,
|
|
||||||
source: 'PeeringDB'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) { /* PeeringDB failed, continue */ }
|
|
||||||
|
|
||||||
// Sort: RIPE results first (usually more relevant for ASN lookup), then PeeringDB
|
|
||||||
results.sort((a, b) => {
|
|
||||||
if (a.source === b.source) return 0;
|
|
||||||
return a.source === 'RIPE Stat' ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.writeHead(200);
|
|
||||||
return res.end(JSON.stringify({ q: q, results: results.slice(0, 12) }));
|
|
||||||
} catch(e) {
|
|
||||||
res.writeHead(500); return res.end(JSON.stringify({error: e.message}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/visitors — unique visitor count
|
// GET /api/visitors — unique visitor count
|
||||||
if (reqPath === "/api/visitors" && req.method === "GET") {
|
if (reqPath === "/api/visitors" && req.method === "GET") {
|
||||||
res.setHeader("Content-Type", "application/json");
|
return searchRoutes.handleVisitors(req, res);
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
res.setHeader("Cache-Control", "no-store");
|
|
||||||
const count = trackVisitor(req);
|
|
||||||
res.writeHead(200);
|
|
||||||
return res.end(JSON.stringify({ visitors: count }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPTIONS preflight (CORS)
|
// OPTIONS preflight (CORS)
|
||||||
if (reqPath === '/api/feedback' && req.method === 'OPTIONS') {
|
if (reqPath === "/api/feedback" && req.method === "OPTIONS") {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
return feedbackRoutes.handleOptions(req, res);
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
||||||
res.writeHead(204);
|
|
||||||
return res.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/feedback — submit feedback entry
|
// POST /api/feedback — submit feedback entry
|
||||||
if (reqPath === '/api/feedback' && req.method === 'POST') {
|
if (reqPath === "/api/feedback" && req.method === "POST") {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
return feedbackRoutes.handlePost(req, res);
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
let body = '';
|
|
||||||
req.on('data', function(chunk) { body += chunk; });
|
|
||||||
req.on('end', function() {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(body);
|
|
||||||
if (!data.message || String(data.message).trim().length < 3) {
|
|
||||||
res.writeHead(400);
|
|
||||||
return res.end(JSON.stringify({ ok: false, error: 'Message too short' }));
|
|
||||||
}
|
|
||||||
const entry = {
|
|
||||||
id: Date.now() + '-' + Math.random().toString(36).slice(2, 7),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
category: String(data.category || 'General').slice(0, 50),
|
|
||||||
message: String(data.message || '').slice(0, 2000),
|
|
||||||
name: String(data.name || 'Anonymous').slice(0, 100),
|
|
||||||
asn: data.asn ? String(data.asn).slice(0, 20) : null,
|
|
||||||
ip: req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.socket.remoteAddress || null,
|
|
||||||
ua: String(req.headers['user-agent'] || '').slice(0, 200)
|
|
||||||
};
|
|
||||||
let entries = [];
|
|
||||||
try { entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8')); } catch (_e) { /* no file yet */ }
|
|
||||||
entries.push(entry);
|
|
||||||
fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(entries, null, 2));
|
|
||||||
// Send email async — don't block response
|
|
||||||
sendFeedbackMail(entry).catch(e => console.error('[MAIL] Failed:', e.message));
|
|
||||||
return res.end(JSON.stringify({ ok: true, id: entry.id }));
|
|
||||||
} catch (_e) {
|
|
||||||
res.writeHead(500);
|
|
||||||
return res.end(JSON.stringify({ ok: false, error: 'Server error' }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/feedback?token=... — admin: fetch all entries as JSON
|
// GET /api/feedback?token=... — admin: fetch all entries as JSON
|
||||||
if (reqPath === '/api/feedback' && req.method === 'GET') {
|
if (reqPath === "/api/feedback" && req.method === "GET") {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
return feedbackRoutes.handleGet(req, res, url);
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
const token = url.searchParams.get('token');
|
|
||||||
if (!token || token !== FEEDBACK_TOKEN) {
|
|
||||||
res.writeHead(401);
|
|
||||||
return res.end(JSON.stringify({ ok: false, error: 'Unauthorized' }));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8'));
|
|
||||||
return res.end(JSON.stringify({ ok: true, entries: entries, count: entries.length }));
|
|
||||||
} catch (_e) {
|
|
||||||
return res.end(JSON.stringify({ ok: true, entries: [], count: 0 }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve favicon
|
// Serve favicon
|
||||||
if (reqPath === "/favicon.ico") {
|
if (reqPath === "/favicon.ico") {
|
||||||
res.writeHead(204);
|
return staticRoutes.handleFavicon(req, res);
|
||||||
return res.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lia's Atlas Paradise - Easter egg page
|
// Lia'''s Atlas Paradise - Easter egg page
|
||||||
if (reqPath === "/lia" || reqPath === "/lia/") {
|
if (reqPath === "/lia" || reqPath === "/lia/") {
|
||||||
try {
|
return staticRoutes.handleLia(req, res);
|
||||||
const liaHtml = fs.readFileSync(__dirname + "/public/lia.html", "utf8");
|
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
||||||
return res.end(liaHtml);
|
|
||||||
} catch (_e) {
|
|
||||||
res.writeHead(500);
|
|
||||||
return res.end("lia.html not found");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Lia's Atlas Paradise: Atlas probe coverage endpoint
|
// Lia's Atlas Paradise: Atlas probe coverage endpoint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
70
server/routes/feedback.js
Normal file
70
server/routes/feedback.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { sendFeedbackMail } = require("../../src/backend/services/smtp");
|
||||||
|
|
||||||
|
const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env";
|
||||||
|
const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json";
|
||||||
|
|
||||||
|
function handleOptions(req, res) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
res.writeHead(204);
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/feedback — submit feedback entry
|
||||||
|
function handlePost(req, res) {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
let body = '';
|
||||||
|
req.on('data', function(chunk) { body += chunk; });
|
||||||
|
req.on('end', function() {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
if (!data.message || String(data.message).trim().length < 3) {
|
||||||
|
res.writeHead(400);
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: 'Message too short' }));
|
||||||
|
}
|
||||||
|
const entry = {
|
||||||
|
id: Date.now() + '-' + Math.random().toString(36).slice(2, 7),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
category: String(data.category || 'General').slice(0, 50),
|
||||||
|
message: String(data.message || '').slice(0, 2000),
|
||||||
|
name: String(data.name || 'Anonymous').slice(0, 100),
|
||||||
|
asn: data.asn ? String(data.asn).slice(0, 20) : null,
|
||||||
|
ip: req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.socket.remoteAddress || null,
|
||||||
|
ua: String(req.headers['user-agent'] || '').slice(0, 200)
|
||||||
|
};
|
||||||
|
let entries = [];
|
||||||
|
try { entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8')); } catch (_e) { /* no file yet */ }
|
||||||
|
entries.push(entry);
|
||||||
|
fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(entries, null, 2));
|
||||||
|
// Send email async — don't block response
|
||||||
|
sendFeedbackMail(entry).catch(e => console.error('[MAIL] Failed:', e.message));
|
||||||
|
return res.end(JSON.stringify({ ok: true, id: entry.id }));
|
||||||
|
} catch (_e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: 'Server error' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/feedback?token=... — admin: fetch all entries as JSON
|
||||||
|
function handleGet(req, res, url) {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
if (!token || token !== FEEDBACK_TOKEN) {
|
||||||
|
res.writeHead(401);
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: 'Unauthorized' }));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8'));
|
||||||
|
return res.end(JSON.stringify({ ok: true, entries: entries, count: entries.length }));
|
||||||
|
} catch (_e) {
|
||||||
|
return res.end(JSON.stringify({ ok: true, entries: [], count: 0 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleOptions, handlePost, handleGet };
|
||||||
83
server/routes/search.js
Normal file
83
server/routes/search.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
const { fetchJSONWithRetry } = require("../services/http-helpers");
|
||||||
|
const { PEERINGDB_API_KEY } = require("../services/peeringdb");
|
||||||
|
const { trackVisitor } = require("../visitors");
|
||||||
|
|
||||||
|
// Name Search (RIPE Stat + PeeringDB combined)
|
||||||
|
async function handleSearch(req, res) {
|
||||||
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||||
|
const q = (params.get('q') || '').trim();
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=120');
|
||||||
|
if (!q || q.length < 2) { res.writeHead(400); return res.end(JSON.stringify({error:'query too short'})); }
|
||||||
|
try {
|
||||||
|
const results = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
// Source 1: RIPE Stat searchcomplete (fast, covers ASNs + org names)
|
||||||
|
try {
|
||||||
|
const ripeUrl = 'https://stat.ripe.net/data/searchcomplete/data.json?resource=' + encodeURIComponent(q);
|
||||||
|
const ripeData = await fetchJSONWithRetry(ripeUrl, { timeout: 6000 });
|
||||||
|
const cats = ripeData && ripeData.data && ripeData.data.categories || [];
|
||||||
|
for (var ci = 0; ci < cats.length; ci++) {
|
||||||
|
var suggs = cats[ci].suggestions || [];
|
||||||
|
for (var si = 0; si < suggs.length; si++) {
|
||||||
|
var s = suggs[si];
|
||||||
|
var val = (s.value || '').toString();
|
||||||
|
// Only ASN results
|
||||||
|
if (/^AS\d+$/i.test(val) && !seen.has(val)) {
|
||||||
|
seen.add(val);
|
||||||
|
// Use description (e.g. "FLEXOPTIX, DE") as the display label
|
||||||
|
var ripeName = s.description || s.label || val;
|
||||||
|
results.push({ asn: val.replace(/^AS/i,''), label: ripeName, description: '', source: 'RIPE Stat' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* RIPE Stat failed, continue */ }
|
||||||
|
|
||||||
|
// Source 2: PeeringDB name search (best for network operator names)
|
||||||
|
try {
|
||||||
|
var pdbUrl = 'https://www.peeringdb.com/api/net?name__icontains=' + encodeURIComponent(q) + '&depth=1&limit=10';
|
||||||
|
if (PEERINGDB_API_KEY) pdbUrl += '&key=' + PEERINGDB_API_KEY;
|
||||||
|
const pdbData = await fetchJSONWithRetry(pdbUrl, { timeout: 8000 });
|
||||||
|
var nets = pdbData && pdbData.data || [];
|
||||||
|
for (var ni = 0; ni < nets.length; ni++) {
|
||||||
|
var net = nets[ni];
|
||||||
|
var asnKey = 'AS' + net.asn;
|
||||||
|
if (net.asn && !seen.has(asnKey)) {
|
||||||
|
seen.add(asnKey);
|
||||||
|
var pdbDesc = [net.info_type, net.country].filter(Boolean).join(' · ');
|
||||||
|
results.push({
|
||||||
|
asn: String(net.asn),
|
||||||
|
label: net.name || asnKey,
|
||||||
|
description: pdbDesc,
|
||||||
|
source: 'PeeringDB'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* PeeringDB failed, continue */ }
|
||||||
|
|
||||||
|
// Sort: RIPE results first (usually more relevant for ASN lookup), then PeeringDB
|
||||||
|
results.sort((a, b) => {
|
||||||
|
if (a.source === b.source) return 0;
|
||||||
|
return a.source === 'RIPE Stat' ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ q: q, results: results.slice(0, 12) }));
|
||||||
|
} catch(e) {
|
||||||
|
res.writeHead(500); return res.end(JSON.stringify({error: e.message}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/visitors — unique visitor count
|
||||||
|
function handleVisitors(req, res) {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
const count = trackVisitor(req);
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ visitors: count }));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleSearch, handleVisitors };
|
||||||
61
server/routes/static.js
Normal file
61
server/routes/static.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const REPO_ROOT = path.join(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function handleRoot(req, res, host, reqPath) {
|
||||||
|
// shell.peercortex.org → admin feedback terminal (check first)
|
||||||
|
if (host === 'shell.peercortex.org') {
|
||||||
|
try {
|
||||||
|
const html = fs.readFileSync('/opt/peercortex-app/public/shell.html', 'utf8');
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
return res.end(html);
|
||||||
|
} catch (_e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
return res.end('shell.html not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// v2.peercortex.org → redirect to main domain
|
||||||
|
if (host === 'v2.peercortex.org') {
|
||||||
|
res.writeHead(301, { Location: 'https://peercortex.org' + reqPath });
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
const htmlFile = "index.html";
|
||||||
|
try {
|
||||||
|
const html = fs.readFileSync("/opt/peercortex-app/public/" + htmlFile, "utf8");
|
||||||
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||||
|
return res.end(html);
|
||||||
|
} catch (_e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
return res.end(htmlFile + " not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIndexEditorial(req, res) {
|
||||||
|
try {
|
||||||
|
const html = fs.readFileSync("/opt/peercortex-app/public/index-editorial.html", "utf8");
|
||||||
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||||
|
return res.end(html);
|
||||||
|
} catch (_e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
return res.end("index-editorial.html not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFavicon(req, res) {
|
||||||
|
res.writeHead(204);
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLia(req, res) {
|
||||||
|
try {
|
||||||
|
const liaHtml = fs.readFileSync(path.join(REPO_ROOT, "public", "lia.html"), "utf8");
|
||||||
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||||
|
return res.end(liaHtml);
|
||||||
|
} catch (_e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
return res.end("lia.html not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleRoot, handleIndexEditorial, handleFavicon, handleLia };
|
||||||
Loading…
x
Reference in New Issue
Block a user