const express = require('express'); const router = express.Router(); const pool = require('../db'); // In-memory cache for customer users fetched from LDAP let ldapCustomerUsersCache = []; // Helper for OTRS CE GenericInterface REST API calls async function otrsRequest(method, path, bodyData = {}) { const OTRS_API_USER = process.env.OTRS_API_USER; const OTRS_API_PASSWORD = process.env.OTRS_API_PASSWORD; const OTRS_API_URL = process.env.OTRS_API_URL; if (!OTRS_API_URL || !OTRS_API_USER) { return null; } const url = `${OTRS_API_URL.replace(/\/$/, '')}${path.startsWith('/') ? path : '/' + path}`; const payload = { UserLogin: OTRS_API_USER, Password: OTRS_API_PASSWORD, ...bodyData }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); throw new Error(`OTRS REST API error (${response.status}): ${errorText}`); } return await response.json(); } catch (err) { clearTimeout(timeoutId); if (err.name === 'AbortError') { throw new Error('OTRS REST API request timed out (15s limit exceeded)'); } throw err; } } // GET /api/queues — Active queues router.get('/queues', async (req, res) => { try { const result = await pool.query( `SELECT q.id, q.name, q.comments FROM queue q WHERE q.valid_id = 1 ORDER BY q.name` ); res.json(result.rows); } catch (err) { console.error('Error fetching queues:', err); res.status(500).json({ error: err.message }); } }); // GET /api/states — Ticket states with state type router.get('/states', async (req, res) => { try { const result = await pool.query( `SELECT ts.id, ts.name, tst.name AS type_name FROM ticket_state ts JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE ts.valid_id = 1 ORDER BY ts.id` ); res.json(result.rows); } catch (err) { console.error('Error fetching states:', err); res.status(500).json({ error: err.message }); } }); // GET /api/priorities — Ticket priorities router.get('/priorities', async (req, res) => { try { const result = await pool.query( `SELECT id, name, color FROM ticket_priority WHERE valid_id = 1 ORDER BY id` ); res.json(result.rows); } catch (err) { console.error('Error fetching priorities:', err); res.status(500).json({ error: err.message }); } }); // GET /api/users — Active agents/operators router.get('/users', async (req, res) => { try { const result = await pool.query( `SELECT id, login, first_name, last_name, title FROM users WHERE valid_id = 1 ORDER BY last_name, first_name` ); res.json(result.rows); } catch (err) { console.error('Error fetching users:', err); res.status(500).json({ error: err.message }); } }); // GET /api/config — Application config router.get('/config', (req, res) => { res.json({ defaultAgentLogin: process.env.OTRS_API_USER || '', dailyTargetTime: parseInt(process.env.DAILY_TARGET_TIME, 10) || 480 }); }); // GET /api/users/time-today — Get sum of today's time units for active agent router.get('/users/time-today', async (req, res) => { try { const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1; const result = await pool.query( `SELECT COALESCE(SUM(time_unit), 0) AS total_today FROM time_accounting WHERE create_by = $1 AND DATE(create_time) = CURRENT_DATE`, [operatorId] ); res.json({ totalToday: parseFloat(result.rows[0].total_today) }); } catch (err) { console.error('Error fetching today\'s time units:', err); res.status(500).json({ error: err.message }); } }); // GET /api/types — Ticket types router.get('/types', async (req, res) => { try { const result = await pool.query( `SELECT id, name FROM ticket_type WHERE valid_id = 1 ORDER BY name` ); res.json(result.rows); } catch (err) { console.error('Error fetching types:', err); res.status(500).json({ error: err.message }); } }); // GET /api/lock-types — Ticket lock types router.get('/lock-types', async (req, res) => { try { const result = await pool.query( `SELECT id, name FROM ticket_lock_type WHERE valid_id = 1 ORDER BY id` ); res.json(result.rows); } catch (err) { console.error('Error fetching lock types:', err); res.status(500).json({ error: err.message }); } }); // GET /api/customer-companies/search — Search customer companies router.get('/customer-companies/search', async (req, res) => { try { const { q = '' } = req.query; let result; if (!q) { result = await pool.query( `SELECT customer_id, name FROM customer_company WHERE valid_id = 1 ORDER BY name LIMIT 20` ); } else { const searchTerm = `%${q}%`; result = await pool.query( `SELECT customer_id, name FROM customer_company WHERE valid_id = 1 AND ( customer_id ILIKE $1 OR name ILIKE $1 ) ORDER BY name LIMIT 20`, [searchTerm] ); } res.json(result.rows); } catch (err) { console.error('Error searching customer companies:', err); res.status(500).json({ error: err.message }); } }); // GET /api/customer-users/search — Search customer users (supports LDAP via REST, fallbacks to DB) router.get('/customer-users/search', async (req, res) => { const { q = '', customer_company_id } = req.query; // 1. Try to search via OTRS GenericInterface REST API if configured if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) { // Search the in-memory cache first if it contains data if (ldapCustomerUsersCache.length > 0) { const searchTerms = q.toLowerCase().split(/\s+/).filter(Boolean); let matchedUsers = ldapCustomerUsersCache; if (searchTerms.length > 0) { matchedUsers = matchedUsers.filter(u => { const login = (u.login || '').toLowerCase(); const email = (u.email || '').toLowerCase(); const first = (u.first_name || '').toLowerCase(); const last = (u.last_name || '').toLowerCase(); const company = (u.customer_id || '').toLowerCase(); return searchTerms.every(term => login.includes(term) || email.includes(term) || first.includes(term) || last.includes(term) || company.includes(term) ); }); } if (customer_company_id) { matchedUsers = matchedUsers.filter(u => u.customer_id === customer_company_id); } return res.json(matchedUsers.slice(0, 20)); } // Fallback to live API search if cache is not yet populated try { const searchPattern = q ? `*${q}*` : '*'; const searchPayload = { Search: searchPattern, Valid: 1 }; if (customer_company_id) { searchPayload.CustomerID = customer_company_id; } const searchResult = await otrsRequest('POST', '/CustomerUserSearch', searchPayload); const searchData = (searchResult && searchResult.Data) || searchResult; if (searchData && searchData.CustomerUserID) { let logins = searchData.CustomerUserID; if (!Array.isArray(logins)) { logins = [logins]; } logins = logins.slice(0, 20); // limit to 20 results // Fetch details for each login in parallel const detailsPromises = logins.map(async (login) => { try { const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: login }); const getData = (getResult && getResult.Data) || getResult; if (getData && getData.CustomerUser) { const u = getData.CustomerUser; return { login: u.UserLogin, email: u.UserEmail || '', first_name: u.UserFirstname || '', last_name: u.UserLastname || '', customer_id: u.UserCustomerID || '' }; } } catch (getErr) { console.warn(`Failed to fetch details for customer user ${login}:`, getErr.message); } return null; }); const users = (await Promise.all(detailsPromises)).filter(u => u !== null); return res.json(users); } } catch (restErr) { console.warn('Failed to search customer users via REST API, falling back to database query:', restErr.message); } } // 2. Fallback to local DB query try { let queryText; let queryParams; if (q) { const searchTerm = `%${q}%`; queryText = ` SELECT login, email, first_name, last_name, customer_id FROM customer_user WHERE valid_id = 1 AND ( login ILIKE $1 OR email ILIKE $1 OR first_name ILIKE $1 OR last_name ILIKE $1 ) `; queryParams = [searchTerm]; if (customer_company_id) { queryText += ` AND customer_id = $2`; queryParams.push(customer_company_id); } } else { queryText = ` SELECT login, email, first_name, last_name, customer_id FROM customer_user WHERE valid_id = 1 `; queryParams = []; if (customer_company_id) { queryText += ` AND customer_id = $1`; queryParams.push(customer_company_id); } } queryText += ` ORDER BY last_name, first_name LIMIT 20`; const result = await pool.query(queryText, queryParams); res.json(result.rows); } catch (err) { console.error('Error searching customer users:', err); res.status(500).json({ error: err.message }); } }); // GET /api/agents/search — Search active agents router.get('/agents/search', async (req, res) => { try { const { q } = req.query; const searchTerm = q ? `%${q}%` : '%'; const result = await pool.query( `SELECT id, login, first_name, last_name FROM users WHERE valid_id = 1 AND ( login ILIKE $1 OR first_name ILIKE $1 OR last_name ILIKE $1 ) ORDER BY last_name, first_name LIMIT 20`, [searchTerm] ); res.json(result.rows); } catch (err) { console.error('Error searching agents:', err); res.status(500).json({ error: err.message }); } }); // GET /api/queues/search — Search active queues router.get('/queues/search', async (req, res) => { try { const { q } = req.query; const searchTerm = q ? `%${q}%` : '%'; const result = await pool.query( `SELECT id, name FROM queue WHERE valid_id = 1 AND name ILIKE $1 ORDER BY name LIMIT 20`, [searchTerm] ); res.json(result.rows); } catch (err) { console.error('Error searching queues:', err); res.status(500).json({ error: err.message }); } }); // GET /api/states/search — Search active ticket states router.get('/states/search', async (req, res) => { try { const { q } = req.query; const searchTerm = q ? `%${q}%` : '%'; const result = await pool.query( `SELECT id, name FROM ticket_state WHERE valid_id = 1 AND name ILIKE $1 ORDER BY name LIMIT 20`, [searchTerm] ); res.json(result.rows); } catch (err) { console.error('Error searching states:', err); res.status(500).json({ error: err.message }); } }); // Helper function to sync customer users from LDAP (OTRS API) into in-memory cache async function syncCustomerUsers() { if (!process.env.OTRS_API_URL || !process.env.OTRS_API_USER) { console.log('[LDAP Sync] OTRS API not configured, skipping customer user sync.'); return 0; } console.log('[LDAP Sync] Starting customer user synchronization...'); try { const searchResult = await otrsRequest('POST', '/CustomerUserSearch', { Search: '*', Valid: 1 }); const searchData = (searchResult && searchResult.Data) || searchResult; if (!searchData || !searchData.CustomerUserID) { console.log('[LDAP Sync] No customer users found to sync.'); return 0; } let logins = searchData.CustomerUserID; if (!Array.isArray(logins)) { logins = [logins]; } console.log(`[LDAP Sync] Found ${logins.length} customer users. Fetching details...`); const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); const newCache = []; // Fetch details in batches of 3 to avoid overloading the OTRS CGI server const batchSize = 3; for (let i = 0; i < logins.length; i += batchSize) { const batchLogins = logins.slice(i, i + batchSize); await Promise.all(batchLogins.map(async (login) => { let retries = 2; while (retries >= 0) { try { const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: login }); const getData = (getResult && getResult.Data) || getResult; if (getData && getData.CustomerUser) { const u = getData.CustomerUser; newCache.push({ login: u.UserLogin, email: u.UserEmail || '', first_name: u.UserFirstname || '', last_name: u.UserLastname || '', customer_id: u.UserCustomerID || '' }); } break; // Success, break retry loop } catch (getErr) { if (retries === 0) { console.warn(`[LDAP Sync] Failed to sync customer user ${login} after retries:`, getErr.message); } else { await delay(200); // Wait 200ms before retrying } retries--; } } })); // Add a small delay between batches await delay(50); } ldapCustomerUsersCache = newCache; console.log(`[LDAP Sync] Completed. Synced ${ldapCustomerUsersCache.length} users to in-memory cache.`); return ldapCustomerUsersCache.length; } catch (err) { console.error('[LDAP Sync] Error during customer user sync:', err); throw err; } } // POST /api/customer-users/sync — Sync customer users from LDAP router.post('/customer-users/sync', async (req, res) => { try { const count = await syncCustomerUsers(); res.json({ message: `Sincronizzazione completata! ${count} utenti sincronizzati.`, count }); } catch (err) { res.status(500).json({ error: 'Errore durante la sincronizzazione', message: err.message }); } }); // Run LDAP synchronization in the background on startup (after 5 seconds) setTimeout(() => { syncCustomerUsers().catch(err => console.error('Startup LDAP sync failed:', err)); }, 5000); module.exports = router;