- Move service account credentials to .env (loaded server-side only) - server.js: inject credentials in proxy, strip any client-provided creds, replace deprecated url.parse with new URL - auth.js / dashboard.js: remove all hardcoded passwords from client code - dashboard.js: remove broken category filter, fix redundant user.info call (use stored userId), add HTML escaping against XSS - login.html: remove unused password field - dashboard.html: remove broken category filter UI - Add .gitignore to exclude .env and node_modules - Add .env.example as configuration template Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
5.5 KiB
JavaScript
163 lines
5.5 KiB
JavaScript
const http = require('http');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Load .env file for local development (ignored if absent)
|
|
try {
|
|
fs.readFileSync(path.join(__dirname, '.env'), 'utf8').split('\n').forEach(line => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) return;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq === -1) return;
|
|
const key = trimmed.slice(0, eq).trim();
|
|
const val = trimmed.slice(eq + 1).trim();
|
|
if (key && !(key in process.env)) process.env[key] = val;
|
|
});
|
|
} catch (_) {}
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const ONE2EDIT_API = 'https://oliver.one2edit.com/v3/Api.php';
|
|
const SERVICE_USERNAME = process.env.SERVICE_USERNAME;
|
|
const SERVICE_PASSWORD = process.env.SERVICE_PASSWORD;
|
|
|
|
if (!SERVICE_USERNAME || !SERVICE_PASSWORD) {
|
|
console.error('ERROR: SERVICE_USERNAME and SERVICE_PASSWORD environment variables are required.');
|
|
console.error('Copy .env.example to .env and fill in the values.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const MIME_TYPES = {
|
|
'.html': 'text/html',
|
|
'.js': 'text/javascript',
|
|
'.css': 'text/css',
|
|
'.json': 'application/json',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.svg': 'image/svg+xml',
|
|
};
|
|
|
|
const CORS_HEADERS = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
};
|
|
|
|
// Strip client-provided credentials and inject service account creds when needed.
|
|
// Requests that carry a sessionId authenticate via user session — no service creds needed.
|
|
function injectCredentials(params) {
|
|
params.delete('authUsername');
|
|
params.delete('authPassword');
|
|
if (!params.has('sessionId')) {
|
|
if (!params.has('authDomain')) params.set('authDomain', 'local');
|
|
params.set('authUsername', SERVICE_USERNAME);
|
|
params.set('authPassword', SERVICE_PASSWORD);
|
|
}
|
|
}
|
|
|
|
function handleApiResponse(res) {
|
|
return (apiRes) => {
|
|
if (apiRes.statusCode === 301 || apiRes.statusCode === 302) {
|
|
res.writeHead(401, { 'Content-Type': 'text/xml;charset=UTF-8', ...CORS_HEADERS });
|
|
res.end('<?xml version="1.0" encoding="UTF-8"?><error><message>Authentication failed</message></error>');
|
|
return;
|
|
}
|
|
const chunks = [];
|
|
apiRes.on('data', chunk => chunks.push(chunk));
|
|
apiRes.on('end', () => {
|
|
const contentType = apiRes.headers['content-type'] || 'text/xml;charset=UTF-8';
|
|
res.writeHead(apiRes.statusCode, { 'Content-Type': contentType, ...CORS_HEADERS });
|
|
res.end(Buffer.concat(chunks));
|
|
});
|
|
};
|
|
}
|
|
|
|
function handleProxyError(res) {
|
|
return (err) => {
|
|
console.error('Proxy error:', err.message);
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
res.end('Error connecting to API');
|
|
};
|
|
}
|
|
|
|
function proxyGet(queryString, res) {
|
|
const params = new URLSearchParams(queryString);
|
|
injectCredentials(params);
|
|
|
|
const apiUrl = `${ONE2EDIT_API}?${params.toString()}`;
|
|
const logUrl = apiUrl.replace(/authPassword=[^&]*/g, 'authPassword=***');
|
|
console.log('Proxy GET:', logUrl.substring(0, 200));
|
|
|
|
https.get(apiUrl, handleApiResponse(res)).on('error', handleProxyError(res));
|
|
}
|
|
|
|
function proxyPost(body, res) {
|
|
const params = new URLSearchParams(body);
|
|
injectCredentials(params);
|
|
|
|
const newBody = params.toString();
|
|
const logBody = newBody.replace(/authPassword=[^&]*/g, 'authPassword=***');
|
|
console.log('Proxy POST:', logBody.substring(0, 200));
|
|
|
|
const apiUrl = new URL(ONE2EDIT_API);
|
|
const options = {
|
|
hostname: apiUrl.hostname,
|
|
path: apiUrl.pathname,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Content-Length': Buffer.byteLength(newBody),
|
|
},
|
|
};
|
|
|
|
const apiReq = https.request(options, handleApiResponse(res));
|
|
apiReq.on('error', handleProxyError(res));
|
|
apiReq.write(newBody);
|
|
apiReq.end();
|
|
}
|
|
|
|
function serveStaticFile(filePath, res) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
|
|
fs.readFile(filePath, (err, content) => {
|
|
if (err) {
|
|
const status = err.code === 'ENOENT' ? 404 : 500;
|
|
res.writeHead(status, { 'Content-Type': 'text/plain' });
|
|
res.end(err.code === 'ENOENT' ? '404 - File Not Found' : '500 - Internal Server Error');
|
|
} else {
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
res.end(content);
|
|
}
|
|
});
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
const parsedUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
const { pathname } = parsedUrl;
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(204, CORS_HEADERS);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/api') {
|
|
if (req.method === 'POST') {
|
|
const chunks = [];
|
|
req.on('data', chunk => chunks.push(chunk));
|
|
req.on('end', () => proxyPost(Buffer.concat(chunks).toString(), res));
|
|
} else {
|
|
proxyGet(parsedUrl.search.slice(1), res);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const filePath = pathname === '/' ? './login.html' : '.' + pathname;
|
|
serveStaticFile(filePath, res);
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`Server running at http://localhost:${PORT}`);
|
|
});
|