3m-portal/dashboard.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

352 lines
14 KiB
JavaScript

function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function handleEdit(jobId) {
window.location.href = `editor.html?jobId=${encodeURIComponent(jobId)}`;
}
async function handlePDF(jobId) {
const jobCard = document.querySelector(`[data-job-id="${jobId}"]`);
const pdfBtn = jobCard ? jobCard.querySelector('.btn-pdf') : null;
if (pdfBtn) {
pdfBtn.disabled = true;
pdfBtn.textContent = 'Downloading...';
}
showPdfPopup();
try {
const job = allJobs.find(j => j.id === jobId);
if (!job) throw new Error('Job not found');
const authConfigStr = sessionStorage.getItem('authConfig');
const externSessionId = sessionStorage.getItem('externSessionId');
if (!authConfigStr || !externSessionId) {
sessionStorage.clear();
window.location.href = 'login.html';
return;
}
const authConfig = JSON.parse(authConfigStr);
const params = new URLSearchParams({
command: 'job.export.pdf',
authDomain: 'local',
sessionId: externSessionId,
clientId: '7',
id: jobId,
result: 'file',
});
const response = await fetch(`${authConfig.apiBaseUrl}?${params}`);
if (!response.ok) throw new Error(`Failed to generate PDF (${response.status})`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = `${job.title || job.documentName || 'document'}_${jobId}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error('PDF export error:', error);
alert('Failed to export PDF. Please try again.');
} finally {
if (pdfBtn) {
pdfBtn.disabled = false;
pdfBtn.textContent = 'PDF';
}
}
}
function showPdfPopup() {
const overlay = document.createElement('div');
overlay.className = 'pdf-popup-overlay';
const popup = document.createElement('div');
popup.className = 'pdf-popup';
popup.innerHTML = `
<p class="pdf-popup-message">PDF download requested</p>
<button class="pdf-popup-ok-btn">OK</button>
`;
overlay.appendChild(popup);
document.body.appendChild(overlay);
requestAnimationFrame(() => overlay.classList.add('active'));
const close = () => {
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 200);
};
popup.querySelector('.pdf-popup-ok-btn').addEventListener('click', close);
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
}
document.addEventListener('DOMContentLoaded', () => {
checkAuthentication();
loadJobsFromAPI();
setupEventListeners();
});
function checkAuthentication() {
if (sessionStorage.getItem('isAuthenticated') !== 'true') {
window.location.href = 'login.html';
}
}
function setupEventListeners() {
document.getElementById('refreshBtn').addEventListener('click', loadJobsFromAPI);
document.getElementById('signOutBtn').addEventListener('click', handleSignOut);
}
async function handleSignOut() {
const signOutBtn = document.getElementById('signOutBtn');
signOutBtn.disabled = true;
signOutBtn.textContent = 'Signing out...';
try {
const externSessionId = sessionStorage.getItem('externSessionId');
const authConfigStr = sessionStorage.getItem('authConfig');
if (externSessionId && authConfigStr) {
const authConfig = JSON.parse(authConfigStr);
const params = new URLSearchParams({
command: 'user.session.extern.remove',
authDomain: 'local',
clientId: '7',
externSessionId,
});
await fetch(`${authConfig.apiBaseUrl}?${params}`, { headers: { Accept: 'text/xml' } });
}
} catch (error) {
console.error('Sign out error:', error);
} finally {
sessionStorage.clear();
window.location.href = 'login.html';
}
}
let allJobs = [];
let filteredJobs = [];
async function loadJobsFromAPI() {
const loadingMessage = document.getElementById('loadingMessage');
const errorMessage = document.getElementById('errorMessage');
const jobsGrid = document.getElementById('jobsGrid');
const emptyState = document.getElementById('emptyState');
loadingMessage.style.display = 'block';
errorMessage.style.display = 'none';
emptyState.style.display = 'none';
jobsGrid.innerHTML = '';
try {
const authConfigStr = sessionStorage.getItem('authConfig');
const userId = sessionStorage.getItem('userId');
const externSessionId = sessionStorage.getItem('externSessionId');
if (!authConfigStr || !userId || !externSessionId) {
sessionStorage.clear();
window.location.href = 'login.html';
return;
}
const authConfig = JSON.parse(authConfigStr);
const params = new URLSearchParams({
command: 'job.list',
authDomain: 'local',
clientId: '7',
userId,
includeDocumentInfos: '1',
includeDocumentPreviews: '1',
includeDocumentWorkflows: '1',
includeDocumentMetadata: '1',
});
params.append('status[0]', 'STARTED');
params.append('status[1]', 'RUNNING');
const response = await fetch(`${authConfig.apiBaseUrl}?${params}`, {
headers: { Accept: 'text/xml' },
});
const xmlDoc = new DOMParser().parseFromString(await response.text(), 'text/xml');
const errorNode = xmlDoc.querySelector('error');
if (errorNode) {
throw new Error(errorNode.querySelector('message')?.textContent || 'Failed to load jobs');
}
allJobs = Array.from(xmlDoc.querySelectorAll('job')).map(parseJobFromXML);
filteredJobs = allJobs;
loadingMessage.style.display = 'none';
if (allJobs.length === 0) {
emptyState.style.display = 'block';
} else {
renderJobs();
}
} catch (error) {
console.error('Failed to load jobs:', error);
loadingMessage.style.display = 'none';
errorMessage.textContent = error.message || 'Failed to load jobs. Please try again.';
errorMessage.style.display = 'block';
}
}
function parseJobFromXML(jobNode) {
const get = sel => jobNode.querySelector(sel)?.textContent || '';
const previewNode = jobNode.querySelector('document > preview');
const thumbnail = previewNode?.textContent
? `data:image/jpeg;base64,${previewNode.textContent}`
: null;
const documentVersion = get('document > version');
const documentName = get('document > name');
const jobName = get('name');
return {
id: get('id'),
documentId: get('document > id'),
title: documentVersion || documentName || jobName || 'Untitled',
documentName: documentVersion || documentName || 'N/A',
language: get('translationLanguage > name') || 'N/A',
pages: get('document > pages') || 'N/A',
status: get('status'),
isOpened: get('isOpened') === 'true',
totalItems: parseInt(get('totalItems')) || 0,
doneItems: parseInt(get('doneItems')) || 0,
finishItems: parseInt(get('finishItems')) || 0,
lastUserName: get('lastuser > name') || 'N/A',
lastEdit: get('lastedit') || 'N/A',
thumbnail: thumbnail || 'https://via.placeholder.com/300x400/cccccc/666666?text=No+Preview',
};
}
function updateOverallProgress() {
let totalFinishItems = 0;
let totalDoneItems = 0;
let totalAllItems = 0;
allJobs.forEach(job => {
totalFinishItems += job.finishItems;
totalDoneItems += job.doneItems;
totalAllItems += job.totalItems;
});
const finishPercent = totalAllItems > 0 ? (totalFinishItems / totalAllItems * 100) : 0;
const donePercent = totalAllItems > 0 ? (totalDoneItems / totalAllItems * 100) : 0;
const completedItems = totalFinishItems + totalDoneItems;
const overallPercent = totalAllItems > 0 ? Math.round((completedItems / totalAllItems) * 100) : 0;
document.getElementById('overallProgressText').innerHTML =
`<strong>${completedItems} of ${totalAllItems} elements done (${overallPercent}%)</strong>`;
document.getElementById('overallDone').style.width = `${donePercent}%`;
document.getElementById('overallFinish').style.width = `${finishPercent}%`;
document.getElementById('overallRemaining').style.width = `${100 - finishPercent - donePercent}%`;
document.getElementById('overallDoneText').textContent = `Done: ${totalDoneItems} (${Math.round(donePercent)}%)`;
document.getElementById('overallFinishText').textContent = `Finished: ${totalFinishItems} (${Math.round(finishPercent)}%)`;
document.getElementById('overallTotalText').textContent = `Total: ${totalAllItems}`;
}
function renderJobs() {
const jobsGrid = document.getElementById('jobsGrid');
const emptyState = document.getElementById('emptyState');
jobsGrid.innerHTML = '';
if (filteredJobs.length === 0) {
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
filteredJobs.forEach(job => jobsGrid.appendChild(createJobCard(job)));
updateOverallProgress();
}
function createJobCard(job) {
const card = document.createElement('div');
card.className = 'job-card';
card.setAttribute('data-job-id', job.id);
let statusClass = 'status-ready';
const statusLower = job.status.toLowerCase();
if (statusLower === 'running') statusClass = 'status-progress';
else if (statusLower === 'finish') statusClass = 'status-done';
const lockedClass = job.isOpened ? ' job-locked' : '';
const disabledAttr = job.isOpened ? ' disabled' : '';
const lockIcon = job.isOpened ? '<div class="lock-icon">🔒</div>' : '';
const finishPercent = job.totalItems > 0 ? (job.finishItems / job.totalItems * 100) : 0;
const donePercent = job.totalItems > 0 ? (job.doneItems / job.totalItems * 100) : 0;
const completedItems = job.finishItems + job.doneItems;
const overallPercent = job.totalItems > 0 ? Math.round((completedItems / job.totalItems) * 100) : 0;
const title = escapeHtml(job.title);
const documentName = escapeHtml(job.documentName);
const documentId = escapeHtml(job.documentId);
const language = escapeHtml(job.language);
const pages = escapeHtml(job.pages);
const lastUserName = escapeHtml(job.lastUserName);
const lastEdit = escapeHtml(job.lastEdit);
const jobId = escapeHtml(job.id);
const thumbnail = escapeHtml(job.thumbnail);
card.innerHTML = `
<div class="job-card-header">${title}${lockIcon}</div>
<div class="job-preview">
<img src="${thumbnail}" alt="${title}" onerror="this.src='https://via.placeholder.com/300x400/cccccc/666666?text=No+Preview'">
</div>
<div class="job-details">
<p class="job-document-name">${documentName}</p>
<p class="job-info-line"><strong>Document ID:</strong> ${documentId}</p>
<p class="job-info-line"><strong>Language:</strong> ${language}</p>
<p class="job-info-line"><strong>Pages:</strong> ${pages}</p>
<p class="job-info-line"><strong>Last User:</strong> ${lastUserName}</p>
<p class="job-info-line"><strong>Last Edit:</strong> ${lastEdit}</p>
<p class="job-info-line"><strong>${completedItems} of ${job.totalItems} elements done (${overallPercent}%)</strong></p>
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-segment progress-done" style="width: ${donePercent}%"></div>
<div class="progress-segment progress-finish" style="width: ${finishPercent}%"></div>
<div class="progress-segment progress-remaining" style="width: ${100 - finishPercent - donePercent}%"></div>
</div>
<div class="progress-legend">
<span class="legend-item">
<span class="legend-color legend-done"></span>
Done: ${job.doneItems} (${Math.round(donePercent)}%)
</span>
<span class="legend-item">
<span class="legend-color legend-finish"></span>
Finished: ${job.finishItems} (${Math.round(finishPercent)}%)
</span>
<span class="legend-item">
<span class="legend-color legend-remaining"></span>
Total: ${job.totalItems}
</span>
</div>
</div>
<div class="job-actions">
<button class="btn-edit${lockedClass}"${disabledAttr} onclick="handleEdit('${jobId}')">Edit</button>
<button class="btn-pdf${lockedClass}"${disabledAttr} onclick="handlePDF('${jobId}')">PDF</button>
</div>
</div>
`;
return card;
}