Files
roda-student-web-if-develop…/groups/RODA/server.js
T

233 lines
7.7 KiB
JavaScript

const fs = require('fs');
const http = require('http');
const path = require('path');
const { URL } = require('url');
const ROOT_DIR = __dirname;
const PARENT_DIR = path.join(__dirname, '..', '..');
const INDEX_PATH = path.join(ROOT_DIR, 'index.html');
const HOST = process.env.HOST || '127.0.0.1';
const PORT = Number(process.env.PORT || 3000);
const DASHBOARD_TARGET_URL = 'https://dashboard.informatika.untan.ac.id/index.php?penelitian';
const RODA_PATH_PREFIX = '/groups/RODA';
function logServerError(scope, error, extra = {}) {
console.error(`[${scope}]`, error && error.stack ? error.stack : error, extra);
}
function getAppConfig() {
return {
debugLogs: false,
dashboardRuntimeFetch: 'always',
dashboardApiUrl: 'api/dashboard',
dashboardTargetUrl: DASHBOARD_TARGET_URL,
};
}
const DASHBOARD_CACHE_TTL_MS = 5 * 60 * 1000;
let dashboardCache = { html: null, timestamp: 0 };
async function fetchDashboardHtml() {
const now = Date.now();
if (dashboardCache.html && (now - dashboardCache.timestamp) < DASHBOARD_CACHE_TTL_MS) {
return { html: dashboardCache.html, cached: 'HIT' };
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 12000);
try {
const response = await fetch(DASHBOARD_TARGET_URL, {
signal: controller.signal,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP_ERROR_${response.status}`);
}
const html = await response.text();
dashboardCache = { html, timestamp: Date.now() };
return { html, cached: 'MISS' };
} catch (error) {
if (dashboardCache.html) {
return { html: dashboardCache.html, cached: 'STALE' };
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
function injectConfig(html) {
const configScript = `<script>window.APP_CONFIG = ${JSON.stringify(getAppConfig())};</script>`;
return html.replace('</head>', ` ${configScript}\n</head>`);
}
function sendHtml(res) {
try {
const html = fs.readFileSync(INDEX_PATH, 'utf8');
const withConfig = injectConfig(html);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(withConfig);
} catch (error) {
logServerError('sendHtml', error);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Internal Server Error');
}
}
function getMimeType(pathname) {
const ext = path.extname(pathname).toLowerCase();
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
return mimeTypes[ext] || 'application/octet-stream';
}
function sendStaticFile(res, filepath) {
try {
if (!fs.existsSync(filepath)) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not found');
return;
}
const content = fs.readFileSync(filepath);
const mimeType = getMimeType(filepath);
res.writeHead(200, { 'Content-Type': mimeType });
res.end(content);
} catch (error) {
logServerError('sendStaticFile', error, { filepath });
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Internal Server Error');
}
}
function sendNotFound(res) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not found');
}
function getRodaStaticPathname(pathname) {
if (pathname === RODA_PATH_PREFIX || pathname === `${RODA_PATH_PREFIX}/` || pathname === `${RODA_PATH_PREFIX}/index.html`) {
return '/index.html';
}
if (pathname.startsWith(`${RODA_PATH_PREFIX}/`)) {
return pathname.slice(RODA_PATH_PREFIX.length);
}
return pathname;
}
const server = http.createServer((req, res) => {
try {
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
const pathname = requestUrl.pathname;
if (pathname === '/' || pathname === '/index.html' || pathname === RODA_PATH_PREFIX || pathname === `${RODA_PATH_PREFIX}/` || pathname === `${RODA_PATH_PREFIX}/index.html`) {
sendHtml(res);
return;
}
if (pathname === '/api/dashboard' || pathname === `${RODA_PATH_PREFIX}/api/dashboard`) {
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
});
res.end();
return;
}
fetchDashboardHtml()
.then(({ html, cached }) => {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'X-Cache': cached,
});
res.end(html);
})
.catch((error) => {
logServerError('api-dashboard', error);
res.writeHead(502, {
'Content-Type': 'text/plain; charset=utf-8',
'Access-Control-Allow-Origin': '*',
});
res.end('DASHBOARD_FETCH_FAILED');
});
return;
}
// Handle /assets/ from parent directory
if (pathname.startsWith('/assets/')) {
const assetsFilepath = path.join(PARENT_DIR, pathname);
const normalizedAssetsPath = path.normalize(assetsFilepath);
if (!normalizedAssetsPath.startsWith(path.normalize(PARENT_DIR))) {
sendNotFound(res);
return;
}
sendStaticFile(res, normalizedAssetsPath);
return;
}
// Serve static files from ROOT_DIR
const filepath = path.join(ROOT_DIR, getRodaStaticPathname(pathname));
const normalizedPath = path.normalize(filepath);
// Security check: ensure the requested file is within ROOT_DIR
if (!normalizedPath.startsWith(path.normalize(ROOT_DIR))) {
sendNotFound(res);
return;
}
sendStaticFile(res, normalizedPath);
} catch (error) {
logServerError('request-handler', error, { url: req.url });
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Internal Server Error');
}
});
server.on('clientError', (error, socket) => {
logServerError('clientError', error);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(PORT, HOST, () => {
console.log(`Server ready at http://${HOST}:${PORT}`);
});
server.on('error', (error) => {
logServerError('server', error, { host: HOST, port: PORT });
process.exitCode = 1;
});
process.on('uncaughtException', (error) => {
logServerError('uncaughtException', error);
process.exitCode = 1;
});
process.on('unhandledRejection', (reason) => {
logServerError('unhandledRejection', reason instanceof Error ? reason : new Error(String(reason)));
process.exitCode = 1;
});