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>
This commit is contained in:
Vadym Samoilenko 2026-03-11 20:17:49 +00:00
parent c7e786f656
commit 67fcb017cc
7 changed files with 295 additions and 696 deletions

3
.env.example Normal file
View file

@ -0,0 +1,3 @@
SERVICE_USERNAME=
SERVICE_PASSWORD=
PORT=3000

50
.gitignore vendored
View file

@ -1,50 +1,2 @@
# These are some examples of commonly ignored file patterns.
# You should customize this list as applicable to your project.
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Node artifact files
node_modules/
dist/
# Compiled Java class files
*.class
# Compiled Python bytecode
*.py[cod]
# Log files
*.log
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
.idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.DS_Store
# Generated by Windows
Thumbs.db
# Applications
*.app
*.exe
*.war
# Large media files
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv
.env

156
auth.js
View file

@ -1,163 +1,95 @@
// Authentication Configurations
const OLIVER_AUTH_CONFIG = {
apiBaseUrl: '/api', // Use local proxy to avoid CORS issues
authDomain: 'local',
authUsername: 'portal@oliver.agency',
authPassword: 'Sp1d3r26!',
apiBaseUrl: '/api',
clientId: '7',
userId: '9'
};
const TMM_AUTH_CONFIG = {
apiBaseUrl: '/api', // Use local proxy to avoid CORS issues
authDomain: 'local',
authUsername: 'portal@oliver.agency',
authPassword: 'Sp1d3r26!',
apiBaseUrl: '/api',
clientId: '7',
userId: '9'
};
// Handle login with external session
async function handleLogin(authConfig, loginType) {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const username = document.getElementById('username').value.trim();
const errorMessage = document.getElementById('errorMessage');
// Get the correct button elements based on login type
const isOliver = loginType === 'oliver';
const loginButton = document.getElementById(isOliver ? 'oliverLoginButton' : 'tmmLoginButton');
const buttonText = document.getElementById(isOliver ? 'oliverButtonText' : 'tmmButtonText');
const spinner = document.getElementById(isOliver ? 'oliverSpinner' : 'tmmSpinner');
const buttonText = document.getElementById(isOliver ? 'oliverButtonText' : 'tmmButtonText');
const spinner = document.getElementById(isOliver ? 'oliverSpinner' : 'tmmSpinner');
// Hide any previous error messages
errorMessage.style.display = 'none';
// Disable button and show spinner
if (!username) {
errorMessage.textContent = 'Please enter your username.';
errorMessage.style.display = 'block';
return;
}
loginButton.disabled = true;
buttonText.textContent = 'Signing in...';
spinner.style.display = 'inline-block';
try {
// First, call user.info to get the real userId for the entered username
// Step 1: Resolve username → userId
const userInfoParams = new URLSearchParams({
command: 'user.info',
command: 'user.info',
authDomain: 'local',
authUsername: 'portal@oliver.agency',
authPassword: 'Sp1d3r26!',
clientId: '6',
username: username,
domain: 'local'
clientId: '6',
username,
domain: 'local',
});
const userInfoUrl = `${authConfig.apiBaseUrl}?${userInfoParams.toString()}`;
const userInfoResponse = await fetch(userInfoUrl, {
method: 'GET',
headers: {
'Accept': 'text/xml'
}
const userInfoRes = await fetch(`${authConfig.apiBaseUrl}?${userInfoParams}`, {
headers: { Accept: 'text/xml' },
});
const userInfoDoc = new DOMParser().parseFromString(await userInfoRes.text(), 'text/xml');
const userInfoXml = await userInfoResponse.text();
console.log('User Info API Response:', userInfoXml);
const userInfoParser = new DOMParser();
const userInfoDoc = userInfoParser.parseFromString(userInfoXml, 'text/xml');
// Check for errors
const userInfoError = userInfoDoc.querySelector('error');
if (userInfoError) {
const errorMsg = userInfoError.querySelector('message')?.textContent || 'User not found';
throw new Error(errorMsg);
throw new Error(userInfoError.querySelector('message')?.textContent || 'User not found');
}
// Extract userId from user.info response
const userIdNode = userInfoDoc.querySelector('user > id') || userInfoDoc.querySelector('id');
if (!userIdNode) {
throw new Error('User not found. Please check your username.');
}
if (!userIdNode) throw new Error('User not found. Please check your username.');
const userId = userIdNode.textContent;
console.log('User ID from user.info:', userId);
// Build the API URL for user.session.extern.add
const params = new URLSearchParams({
command: 'user.session.extern.add',
authDomain: authConfig.authDomain,
authUsername: authConfig.authUsername,
authPassword: authConfig.authPassword,
clientId: authConfig.clientId,
username: username,
userId: userId
// Step 2: Create external session for the user
const sessionParams = new URLSearchParams({
command: 'user.session.extern.add',
authDomain: 'local',
clientId: authConfig.clientId,
username,
userId,
});
const apiUrl = `${authConfig.apiBaseUrl}?${params.toString()}`;
// Make the API call
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Accept': 'text/xml'
}
const sessionRes = await fetch(`${authConfig.apiBaseUrl}?${sessionParams}`, {
headers: { Accept: 'text/xml' },
});
const sessionDoc = new DOMParser().parseFromString(await sessionRes.text(), 'text/xml');
const xmlText = await response.text();
console.log('API Response:', xmlText);
// Parse XML response
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
// Check for errors in XML
const errorNode = xmlDoc.querySelector('error');
if (errorNode) {
const errorMsg = errorNode.querySelector('message')?.textContent || 'Login failed';
throw new Error(errorMsg);
const sessionError = sessionDoc.querySelector('error');
if (sessionError) {
throw new Error(sessionError.querySelector('message')?.textContent || 'Login failed');
}
// Extract externSessionId from the response
const externSessionIdNode = xmlDoc.querySelector('externSessionId');
const externSessionId = externSessionIdNode?.textContent;
const externSessionId = sessionDoc.querySelector('externSessionId')?.textContent;
if (!externSessionId) throw new Error('No session received from server');
if (!externSessionId) {
throw new Error('No externSessionId received from server');
}
sessionStorage.setItem('isAuthenticated', 'true');
sessionStorage.setItem('username', username);
sessionStorage.setItem('userId', userId);
sessionStorage.setItem('externSessionId', externSessionId);
sessionStorage.setItem('authConfig', JSON.stringify(authConfig));
console.log('Extern Session ID:', externSessionId);
// Check if login was successful
if (response.ok && !errorNode) {
// Store session data in sessionStorage
sessionStorage.setItem('isAuthenticated', 'true');
sessionStorage.setItem('username', username);
sessionStorage.setItem('userId', userId);
sessionStorage.setItem('externSessionId', externSessionId);
sessionStorage.setItem('authConfig', JSON.stringify(authConfig));
// Redirect to dashboard
window.location.href = 'dashboard.html';
} else {
throw new Error('Login failed. Please check your credentials.');
}
window.location.href = 'dashboard.html';
} catch (error) {
console.error('Login error:', error);
// Show error message
errorMessage.textContent = error.message || 'An error occurred. Please try again.';
errorMessage.style.display = 'block';
// Re-enable button
loginButton.disabled = false;
buttonText.textContent = isOliver ? 'Oliver Login' : '3M Login';
spinner.style.display = 'none';
}
}
// Handle Oliver login button click
document.getElementById('oliverLoginButton').addEventListener('click', function() {
handleLogin(OLIVER_AUTH_CONFIG, 'oliver');
});
// Handle 3M login button click
document.getElementById('tmmLoginButton').addEventListener('click', function() {
handleLogin(TMM_AUTH_CONFIG, '3m');
});
document.getElementById('oliverLoginButton').addEventListener('click', () => handleLogin(OLIVER_AUTH_CONFIG, 'oliver'));
document.getElementById('tmmLoginButton').addEventListener('click', () => handleLogin(TMM_AUTH_CONFIG, '3m'));

View file

@ -53,18 +53,6 @@
</div>
</div>
<!-- Category Filter -->
<div class="category-filter">
<label for="categorySelect">Category</label>
<select id="categorySelect">
<option value="all">All</option>
<option value="catalogue">Catalogue</option>
<option value="brochure">Brochure</option>
<option value="flyer">Flyer</option>
<option value="poster">Poster</option>
</select>
</div>
<!-- Loading Message -->
<div id="loadingMessage" class="loading-message" style="display: none;">
<p>Loading jobs...</p>

View file

@ -1,34 +1,31 @@
// Global functions for button handlers
function handleEdit(jobId) {
console.log('Edit job:', jobId);
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// Navigate to editor page in the same window
window.location.href = `editor.html?jobId=${jobId}`;
function handleEdit(jobId) {
window.location.href = `editor.html?jobId=${encodeURIComponent(jobId)}`;
}
async function handlePDF(jobId) {
console.log('Generate PDF for job:', jobId);
// Find the PDF button for this job and disable it
const jobCard = document.querySelector(`[data-job-id="${jobId}"]`);
const pdfBtn = jobCard ? jobCard.querySelector('.btn-pdf') : null;
const pdfBtn = jobCard ? jobCard.querySelector('.btn-pdf') : null;
if (pdfBtn) {
pdfBtn.disabled = true;
pdfBtn.textContent = 'Downloading...';
}
// Show popup immediately so user knows the button was clicked
showPdfPopup();
try {
// Find the job
const job = allJobs.find(j => j.id === jobId);
if (!job) {
throw new Error('Job not found');
}
if (!job) throw new Error('Job not found');
// Get auth config from session
const authConfigStr = sessionStorage.getItem('authConfig');
const authConfigStr = sessionStorage.getItem('authConfig');
const externSessionId = sessionStorage.getItem('externSessionId');
if (!authConfigStr || !externSessionId) {
@ -39,45 +36,31 @@ async function handlePDF(jobId) {
const authConfig = JSON.parse(authConfigStr);
// Build params for job.export.pdf using session-based auth
const params = new URLSearchParams({
command: 'job.export.pdf',
command: 'job.export.pdf',
authDomain: 'local',
sessionId: externSessionId,
clientId: '7',
id: jobId,
result: 'file'
sessionId: externSessionId,
clientId: '7',
id: jobId,
result: 'file',
});
// Fetch the PDF using GET
const apiUrl = `${authConfig.apiBaseUrl}?${params.toString()}`;
const response = await fetch(apiUrl, {
method: 'GET'
});
const response = await fetch(`${authConfig.apiBaseUrl}?${params}`);
if (!response.ok) throw new Error(`Failed to generate PDF (${response.status})`);
if (!response.ok) {
const errorText = await response.text();
console.error('PDF export API error:', response.status, errorText);
throw new Error(`Failed to generate PDF (${response.status})`);
}
const blob = await response.blob();
// Create a temporary URL for the blob and trigger download
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
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 {
// Re-enable the PDF button
if (pdfBtn) {
pdfBtn.disabled = false;
pdfBtn.textContent = 'PDF';
@ -85,13 +68,10 @@ async function handlePDF(jobId) {
}
}
// Show PDF download confirmation popup
function showPdfPopup() {
// Create overlay
const overlay = document.createElement('div');
overlay.className = 'pdf-popup-overlay';
// Create popup
const popup = document.createElement('div');
popup.className = 'pdf-popup';
popup.innerHTML = `
@ -101,339 +81,188 @@ function showPdfPopup() {
overlay.appendChild(popup);
document.body.appendChild(overlay);
// Show with active class for animation
requestAnimationFrame(() => overlay.classList.add('active'));
// Close on OK button click
popup.querySelector('.pdf-popup-ok-btn').addEventListener('click', () => {
const close = () => {
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 200);
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
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(); });
}
// Check authentication on page load
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', () => {
checkAuthentication();
loadJobsFromAPI();
setupEventListeners();
});
// Check if user is authenticated
function checkAuthentication() {
const isAuthenticated = sessionStorage.getItem('isAuthenticated');
if (!isAuthenticated || isAuthenticated !== 'true') {
// Redirect to login if not authenticated
if (sessionStorage.getItem('isAuthenticated') !== 'true') {
window.location.href = 'login.html';
}
}
// Setup event listeners
function setupEventListeners() {
// Refresh button
document.getElementById('refreshBtn').addEventListener('click', loadJobsFromAPI);
// Sign out button
document.getElementById('signOutBtn').addEventListener('click', handleSignOut);
// Category filter
document.getElementById('categorySelect').addEventListener('change', filterJobs);
}
// Handle sign out
async function handleSignOut() {
const signOutBtn = document.getElementById('signOutBtn');
signOutBtn.disabled = true;
signOutBtn.textContent = 'Signing out...';
try {
// Get session data
const externSessionId = sessionStorage.getItem('externSessionId');
const authConfigStr = sessionStorage.getItem('authConfig');
const authConfigStr = sessionStorage.getItem('authConfig');
if (!externSessionId || !authConfigStr) {
throw new Error('Session data not found');
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' } });
}
const authConfig = JSON.parse(authConfigStr);
// Build the API URL for sign out using credential-based auth
const params = new URLSearchParams({
command: 'user.session.extern.remove',
authDomain: 'local',
authUsername: 'portal@oliver.agency',
authPassword: 'Sp1d3r26!',
clientId: '7',
externSessionId: externSessionId
});
const apiUrl = `${authConfig.apiBaseUrl}?${params.toString()}`;
// Make the API call
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Accept': 'text/xml'
}
});
// Clear session data regardless of API response
sessionStorage.clear();
// Redirect to login
window.location.href = 'login.html';
} catch (error) {
console.error('Sign out error:', error);
// Clear session and redirect anyway
} finally {
sessionStorage.clear();
window.location.href = 'login.html';
}
}
// Store for filtered jobs
let allJobs = [];
let allJobs = [];
let filteredJobs = [];
// Load jobs from API
async function loadJobsFromAPI() {
const loadingMessage = document.getElementById('loadingMessage');
const errorMessage = document.getElementById('errorMessage');
const jobsGrid = document.getElementById('jobsGrid');
const emptyState = document.getElementById('emptyState');
const errorMessage = document.getElementById('errorMessage');
const jobsGrid = document.getElementById('jobsGrid');
const emptyState = document.getElementById('emptyState');
// Show loading
loadingMessage.style.display = 'block';
errorMessage.style.display = 'none';
emptyState.style.display = 'none';
jobsGrid.innerHTML = '';
errorMessage.style.display = 'none';
emptyState.style.display = 'none';
jobsGrid.innerHTML = '';
try {
// Get auth config and username from session
const authConfigStr = sessionStorage.getItem('authConfig');
const username = sessionStorage.getItem('username');
const authConfigStr = sessionStorage.getItem('authConfig');
const userId = sessionStorage.getItem('userId');
const externSessionId = sessionStorage.getItem('externSessionId');
if (!authConfigStr || !username) {
throw new Error('Session data not found');
if (!authConfigStr || !userId || !externSessionId) {
sessionStorage.clear();
window.location.href = 'login.html';
return;
}
const authConfig = JSON.parse(authConfigStr);
const externSessionId = sessionStorage.getItem('externSessionId');
if (!externSessionId) {
throw new Error('Session expired');
}
// First, call user.info to get the userId for the logged-in user
const userInfoParams = new URLSearchParams({
command: 'user.info',
authDomain: 'local',
authUsername: 'portal@oliver.agency',
authPassword: 'Sp1d3r26!',
clientId: '6',
username: username,
domain: 'local'
});
const userInfoUrl = `${authConfig.apiBaseUrl}?${userInfoParams.toString()}`;
const userInfoResponse = await fetch(userInfoUrl, {
method: 'GET',
headers: {
'Accept': 'text/xml'
}
});
const userInfoXml = await userInfoResponse.text();
console.log('User Info API Response:', userInfoXml);
const userInfoParser = new DOMParser();
const userInfoDoc = userInfoParser.parseFromString(userInfoXml, 'text/xml');
// Check for errors
const userInfoError = userInfoDoc.querySelector('error');
if (userInfoError) {
const errorMsg = userInfoError.querySelector('message')?.textContent || 'Failed to get user info';
throw new Error(errorMsg);
}
// Extract userId from user.info response
const userIdNode = userInfoDoc.querySelector('user > id') || userInfoDoc.querySelector('id');
if (!userIdNode) {
throw new Error('No userId returned from user.info');
}
const userId = userIdNode.textContent;
console.log('User ID from user.info:', userId);
// Build API URL for job.list using credential-based auth
const params = new URLSearchParams({
command: 'job.list',
authDomain: 'local',
authUsername: 'portal@oliver.agency',
authPassword: 'Sp1d3r26!',
clientId: '7',
userId: userId,
includeDocumentInfos: '1',
includeDocumentPreviews: '1',
command: 'job.list',
authDomain: 'local',
clientId: '7',
userId,
includeDocumentInfos: '1',
includeDocumentPreviews: '1',
includeDocumentWorkflows: '1',
includeDocumentMetadata: '1'
includeDocumentMetadata: '1',
});
// Add status filters
params.append('status[0]', 'STARTED');
params.append('status[1]', 'RUNNING');
const apiUrl = `${authConfig.apiBaseUrl}?${params.toString()}`;
// Fetch jobs from API
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Accept': 'text/xml'
}
const response = await fetch(`${authConfig.apiBaseUrl}?${params}`, {
headers: { Accept: 'text/xml' },
});
const xmlDoc = new DOMParser().parseFromString(await response.text(), 'text/xml');
const xmlText = await response.text();
console.log('Jobs API Response:', xmlText);
// Parse XML response
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
// Check for errors
const errorNode = xmlDoc.querySelector('error');
if (errorNode) {
const errorMsg = errorNode.querySelector('message')?.textContent || 'Failed to load jobs';
throw new Error(errorMsg);
throw new Error(errorNode.querySelector('message')?.textContent || 'Failed to load jobs');
}
// Parse jobs from XML
const jobNodes = xmlDoc.querySelectorAll('job');
allJobs = Array.from(jobNodes).map(jobNode => parseJobFromXML(jobNode));
allJobs = Array.from(xmlDoc.querySelectorAll('job')).map(parseJobFromXML);
filteredJobs = allJobs;
// Hide loading
loadingMessage.style.display = 'none';
// Display jobs
if (allJobs.length === 0) {
emptyState.style.display = 'block';
} else {
loadJobsData();
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';
errorMessage.textContent = error.message || 'Failed to load jobs. Please try again.';
errorMessage.style.display = 'block';
}
}
// Parse job data from XML node
function parseJobFromXML(jobNode) {
const getNodeText = (selector) => {
const node = jobNode.querySelector(selector);
return node ? node.textContent : '';
};
const get = sel => jobNode.querySelector(sel)?.textContent || '';
const id = getNodeText('id');
const jobName = getNodeText('name');
const status = getNodeText('status');
// Get document info
const documentId = getNodeText('document > id');
const documentVersion = getNodeText('document > version');
const documentName = getNodeText('document > name');
const pages = getNodeText('document > pages') || 'N/A';
// Get translation language name
const translationLanguageName = getNodeText('translationLanguage > name') || 'N/A';
// Check if job is opened (locked)
const isOpened = getNodeText('isOpened') === 'true';
// Get progress data
const totalItems = parseInt(getNodeText('totalItems')) || 0;
const allItems = parseInt(getNodeText('allItems')) || 0;
const doneItems = parseInt(getNodeText('doneItems')) || 0;
const finishItems = parseInt(getNodeText('finishItems')) || 0;
// Get last user and last edit info
const lastUserName = getNodeText('lastuser > name') || 'N/A';
const lastEdit = getNodeText('lastedit') || 'N/A';
// Get preview from document > preview (base64 image)
const previewNode = jobNode.querySelector('document > preview');
let thumbnail = null;
if (previewNode && previewNode.textContent) {
thumbnail = `data:image/jpeg;base64,${previewNode.textContent}`;
}
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: id,
documentId: documentId,
title: documentVersion || documentName || jobName || 'Untitled',
id: get('id'),
documentId: get('document > id'),
title: documentVersion || documentName || jobName || 'Untitled',
documentName: documentVersion || documentName || 'N/A',
language: translationLanguageName,
pages: pages,
status: status,
isOpened: isOpened,
totalItems: totalItems,
doneItems: doneItems,
finishItems: finishItems,
lastUserName: lastUserName,
lastEdit: lastEdit,
thumbnail: thumbnail || 'https://via.placeholder.com/300x400/cccccc/666666?text=No+Preview'
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',
};
}
// Update overall progress bar
function updateOverallProgress() {
// Calculate totals from all jobs (not filtered)
let totalFinishItems = 0;
let totalDoneItems = 0;
let totalAllItems = 0;
let totalDoneItems = 0;
let totalAllItems = 0;
allJobs.forEach(job => {
totalFinishItems += job.finishItems;
totalDoneItems += job.doneItems;
totalAllItems += job.totalItems;
totalDoneItems += job.doneItems;
totalAllItems += job.totalItems;
});
// Calculate percentages
const finishPercent = totalAllItems > 0 ? (totalFinishItems / totalAllItems * 100) : 0;
const donePercent = totalAllItems > 0 ? (totalDoneItems / totalAllItems * 100) : 0;
const remainingPercent = 100 - finishPercent - donePercent;
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;
const completedItems = totalFinishItems + totalDoneItems;
const overallPercent = totalAllItems > 0 ? Math.round((completedItems / totalAllItems) * 100) : 0;
// Update DOM elements
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 = `${remainingPercent}%`;
document.getElementById('overallDoneText').textContent = `Done: ${totalDoneItems} (${Math.round(donePercent)}%)`;
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}`;
document.getElementById('overallTotalText').textContent = `Total: ${totalAllItems}`;
}
// Load jobs data
function loadJobsData() {
const jobsGrid = document.getElementById('jobsGrid');
function renderJobs() {
const jobsGrid = document.getElementById('jobsGrid');
const emptyState = document.getElementById('emptyState');
// Clear grid
jobsGrid.innerHTML = '';
if (filteredJobs.length === 0) {
@ -442,69 +271,59 @@ function loadJobsData() {
}
emptyState.style.display = 'none';
// Create job cards
filteredJobs.forEach(job => {
const jobCard = createJobCard(job);
jobsGrid.appendChild(jobCard);
});
// Update overall progress bar
filteredJobs.forEach(job => jobsGrid.appendChild(createJobCard(job)));
updateOverallProgress();
}
// Create a job card element
function createJobCard(job) {
const card = document.createElement('div');
card.className = 'job-card';
card.setAttribute('data-job-id', job.id);
// Determine status badge class based on API status
let statusClass = 'status-ready';
const statusLower = job.status.toLowerCase();
if (statusLower === 'running') {
statusClass = 'status-progress';
} else if (statusLower === 'started') {
statusClass = 'status-ready';
} else if (statusLower === 'stopped') {
statusClass = 'status-progress';
} else if (statusLower === 'finish') {
statusClass = 'status-ready';
}
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 lockIcon = job.isOpened ? '<div class="lock-icon">🔒</div>' : '';
// Calculate progress percentages
const finishPercent = job.totalItems > 0 ? (job.finishItems / job.totalItems * 100) : 0;
const donePercent = job.totalItems > 0 ? (job.doneItems / job.totalItems * 100) : 0;
const remainingPercent = 100 - finishPercent - donePercent;
// Calculate overall completion
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">${job.title}${lockIcon}</div>
<div class="job-card-header">${title}${lockIcon}</div>
<div class="job-preview">
<img src="${job.thumbnail}" alt="${job.title}" onerror="this.src='https://via.placeholder.com/300x400/cccccc/666666?text=No+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">${job.documentName}</p>
<p class="job-info-line"><strong>Document ID:</strong> ${job.documentId}</p>
<p class="job-info-line"><strong>Language:</strong> ${job.language}</p>
<p class="job-info-line"><strong>Pages:</strong> ${job.pages}</p>
<p class="job-info-line"><strong>Last User:</strong> ${job.lastUserName}</p>
<p class="job-info-line"><strong>Last Edit:</strong> ${job.lastEdit}</p>
<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: ${remainingPercent}%"></div>
<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">
@ -523,31 +342,11 @@ function createJobCard(job) {
</div>
<div class="job-actions">
<button class="btn-edit${lockedClass}" ${disabledAttr} onclick="handleEdit('${job.id}')">
Edit
</button>
<button class="btn-pdf${lockedClass}" ${disabledAttr} onclick="handlePDF('${job.id}')">
PDF
</button>
<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;
}
// Filter jobs by category
function filterJobs() {
const categorySelect = document.getElementById('categorySelect');
const selectedCategory = categorySelect.value;
if (selectedCategory === 'all') {
filteredJobs = allJobs;
} else {
filteredJobs = allJobs.filter(job =>
job.category.toLowerCase() === selectedCategory.toLowerCase()
);
}
loadJobsData();
}

View file

@ -28,17 +28,6 @@
>
</div>
<div class="form-group">
<input
type="password"
id="password"
name="password"
placeholder="Password"
required
class="form-input"
>
</div>
<div class="button-container">
<button type="button" class="login-button oliver-button" id="oliverLoginButton">
<span id="oliverButtonText">Oliver Login</span>

286
server.js
View file

@ -1,227 +1,163 @@
const http = require('http');
const https = require('https');
const url = require('url');
const fs = require('fs');
const path = require('path');
const PORT = 3000;
const ONE2EDIT_API = 'https://oliver.one2edit.com/v3/Api.php';
// MIME types for static files
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',
};
// Proxy API requests to one2edit (GET)
function proxyApiRequest(queryString, res) {
const apiUrl = `${ONE2EDIT_API}?${queryString}`;
console.log('Proxying GET request to:', apiUrl.replace(/password=[^&]*/g, 'password=***'));
https.get(apiUrl, (apiRes) => {
// Handle redirects
if (apiRes.statusCode === 302 || apiRes.statusCode === 301) {
const redirectUrl = apiRes.headers.location;
console.log('API Redirect to:', redirectUrl);
console.log('Redirect Headers:', apiRes.headers);
// Return error instead of following redirect
res.writeHead(401, {
'Content-Type': 'text/xml;charset=UTF-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
});
res.end('<?xml version="1.0" encoding="UTF-8"?><error><message>Authentication failed - API returned redirect</message></error>');
return;
}
const chunks = [];
apiRes.on('data', (chunk) => {
chunks.push(chunk);
});
apiRes.on('end', () => {
const data = Buffer.concat(chunks);
// Log the response for debugging
console.log('API Response Status:', apiRes.statusCode);
console.log('API Response Length:', data.length);
console.log('API Response Body:', data.toString('utf8').substring(0, 500)); // First 500 chars
// Determine content type from response
const contentType = apiRes.headers['content-type'] || 'text/xml;charset=UTF-8';
// Set CORS headers and forward the actual status code
res.writeHead(apiRes.statusCode, {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
});
res.end(data);
});
}).on('error', (err) => {
console.error('API request error:', err);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error connecting to one2edit API');
// 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);
}
// Proxy API requests to one2edit (POST)
function proxyApiPostRequest(postBody, res) {
const apiUrl = new URL(ONE2EDIT_API);
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',
};
console.log('Proxying POST request to:', ONE2EDIT_API);
console.log('POST body:', postBody.replace(/password=[^&]*/g, 'password=***'));
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
const options = {
hostname: apiUrl.hostname,
path: apiUrl.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postBody)
}
};
// 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);
}
}
const apiReq = https.request(options, (apiRes) => {
// Handle redirects
if (apiRes.statusCode === 302 || apiRes.statusCode === 301) {
const redirectUrl = apiRes.headers.location;
console.log('API Redirect to:', redirectUrl);
res.writeHead(401, {
'Content-Type': 'text/xml;charset=UTF-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
});
res.end('<?xml version="1.0" encoding="UTF-8"?><error><message>Authentication failed - API returned redirect</message></error>');
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('data', chunk => chunks.push(chunk));
apiRes.on('end', () => {
const data = Buffer.concat(chunks);
console.log('API Response Status:', apiRes.statusCode);
console.log('API Response Length:', data.length);
console.log('API Response Body:', data.toString('utf8').substring(0, 500));
const contentType = apiRes.headers['content-type'] || 'text/xml;charset=UTF-8';
res.writeHead(apiRes.statusCode, {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
});
res.end(data);
res.writeHead(apiRes.statusCode, { 'Content-Type': contentType, ...CORS_HEADERS });
res.end(Buffer.concat(chunks));
});
});
};
}
apiReq.on('error', (err) => {
console.error('API POST request error:', err);
function handleProxyError(res) {
return (err) => {
console.error('Proxy error:', err.message);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error connecting to one2edit API');
});
res.end('Error connecting to API');
};
}
apiReq.write(postBody);
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();
}
// Serve static files
function serveStaticFile(filePath, res) {
const extname = String(path.extname(filePath)).toLowerCase();
const contentType = MIME_TYPES[extname] || 'application/octet-stream';
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 - File Not Found');
} else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('500 - Internal Server Error');
}
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, 'utf-8');
res.end(content);
}
});
}
// Create HTTP server
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const parsedUrl = new URL(req.url, `http://localhost:${PORT}`);
const { pathname } = parsedUrl;
// Handle CORS preflight
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
});
res.writeHead(204, CORS_HEADERS);
res.end();
return;
}
// Handle API proxy requests
if (pathname === '/api') {
// Handle POST requests
if (req.method === 'POST') {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const postBody = Buffer.concat(chunks).toString();
proxyApiPostRequest(postBody, res);
});
return;
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => proxyPost(Buffer.concat(chunks).toString(), res));
} else {
proxyGet(parsedUrl.search.slice(1), res);
}
// Handle GET requests
const queryString = parsedUrl.query;
const queryStr = Object.keys(queryString)
.map(key => {
const value = queryString[key];
if (Array.isArray(value)) {
return value.map((v, i) => `${key}[${i}]=${encodeURIComponent(v)}`).join('&');
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
proxyApiRequest(queryStr, res);
return;
}
// Serve static files
let filePath = '.' + pathname;
if (filePath === './') {
filePath = './login.html';
}
const filePath = pathname === '/' ? './login.html' : '.' + pathname;
serveStaticFile(filePath, res);
});
server.listen(PORT, () => {
console.log(`\n🚀 One2Edit Portal Server Running!`);
console.log(`\n📱 Open your browser to: http://localhost:${PORT}`);
console.log(`\n✅ API proxy is handling CORS at: http://localhost:${PORT}/api`);
console.log(`\nPress Ctrl+C to stop the server\n`);
console.log(`Server running at http://localhost:${PORT}`);
});