const express = require('express'); const router = express.Router(); const pool = require('../db'); const { db } = require('../activityDb'); // Ensure SQLite tables for customer user cache exist in internal.db db.exec(` CREATE TABLE IF NOT EXISTS customer_user_cache ( login TEXT PRIMARY KEY, email TEXT, first_name TEXT, last_name TEXT, customer_id TEXT, phone TEXT ); CREATE TABLE IF NOT EXISTS sync_status ( key TEXT PRIMARY KEY, val TEXT ); `); // 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}`); } const result = await response.json(); if (result && result.Error) { throw new Error(`OTRS API Error: ${result.Error.ErrorMessage} (${result.Error.ErrorCode})`); } return result; } 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, phraseThreshold: parseInt(process.env.PHRASE_THRESHOLD, 10) || 70, autoTimeMinHour: process.env.AUTO_TIME_MIN_HOUR || '18:00' }); }); // 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) { try { // Query the local SQLite cache table first const searchTerm = q ? `%${q}%` : '%'; let rows; if (customer_company_id) { rows = db.prepare(` SELECT login, email, first_name, last_name, customer_id FROM customer_user_cache WHERE customer_id = ? AND ( login LIKE ? OR email LIKE ? OR first_name LIKE ? OR last_name LIKE ? ) ORDER BY last_name, first_name LIMIT 20 `).all(customer_company_id, searchTerm, searchTerm, searchTerm, searchTerm); } else { rows = db.prepare(` SELECT login, email, first_name, last_name, customer_id FROM customer_user_cache WHERE login LIKE ? OR email LIKE ? OR first_name LIKE ? OR last_name LIKE ? ORDER BY last_name, first_name LIMIT 20 `).all(searchTerm, searchTerm, searchTerm, searchTerm); } if (rows.length > 0) { return res.json(rows); } } catch (dbErr) { console.warn('[LDAP Cache Search] SQLite query failed, falling back to live API:', dbErr.message); } // 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 SQLite 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)); // Prepare SQLite insert statement with upsert logic const upsertStmt = db.prepare(` INSERT INTO customer_user_cache (login, email, first_name, last_name, customer_id, phone) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(login) DO UPDATE SET email=excluded.email, first_name=excluded.first_name, last_name=excluded.last_name, customer_id=excluded.customer_id, phone=excluded.phone `); let syncCount = 0; // 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; // Run insertion in SQLite cache table upsertStmt.run( u.UserLogin, u.UserEmail || '', u.UserFirstname || '', u.UserLastname || '', u.UserCustomerID || '', u.UserPhone || '' ); syncCount++; } 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); } // Update last sync timestamp in SQLite sync_status table db.prepare(` INSERT INTO sync_status (key, val) VALUES ('last_ldap_sync_time', ?) ON CONFLICT(key) DO UPDATE SET val=excluded.val `).run(new Date().toISOString()); console.log(`[LDAP Sync] Completed. Synced ${syncCount} users to SQLite customer_user_cache.`); return syncCount; } 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) { console.warn('[LDAP Sync] Sincronizzazione non riuscita:', err.message); res.json({ message: `Sincronizzazione LDAP ignorata o non disponibile: ${err.message}`, count: 0 }); } }); // Run LDAP synchronization on startup if last sync was > LDAP_SYNC_INTERVAL_HOURS ago setTimeout(() => { try { const row = db.prepare("SELECT val FROM sync_status WHERE key = 'last_ldap_sync_time'").get(); let shouldSync = true; if (row && row.val) { const lastSync = new Date(row.val); const intervalHours = parseFloat(process.env.LDAP_SYNC_INTERVAL_HOURS) || 24; const thresholdTime = new Date(Date.now() - intervalHours * 60 * 60 * 1000); if (lastSync > thresholdTime) { shouldSync = false; console.log(`[LDAP Sync] Last sync was on ${lastSync.toLocaleString()} (Threshold: ${intervalHours}h). Skipping auto-sync at startup.`); } } if (shouldSync) { syncCustomerUsers().catch(err => console.error('Startup LDAP sync failed:', err)); } } catch (err) { console.error('[LDAP Sync] Failed to check LDAP sync status:', err.message); } }, 5000); module.exports = router;