3m-portal/server.js
Vadym Samoilenko 67fcb017cc Prepare for production: remove hardcoded credentials and fix bugs
- 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>
2026-03-11 20:17:49 +00:00

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}`);
});