require('dotenv').config(); const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const db = require('./db'); const app = express(); const PORT = process.env.PORT || 8180; const JWT_SECRET = process.env.JWT_SECRET || 'super_secret_corporate_token_key_123!'; // Setup default admin credentials const ADMIN_USER = process.env.ADMIN_USERNAME || 'admin'; let ADMIN_PASS = process.env.ADMIN_PASSWORD || 'adminpass'; const ADMIN_PASS_HASH = bcrypt.hashSync(ADMIN_PASS, 10); console.log(`=========================================`); console.log(`Intranet Address Book Server starting...`); console.log(`Admin Username: ${ADMIN_USER}`); console.log(`Admin Password: ${process.env.ADMIN_PASSWORD ? '****** (From Env)' : 'adminpass (Default)'}`); console.log(`Default Port: ${PORT}`); console.log(`=========================================`); app.use(cors()); app.use(express.json()); // Serve React static build files const publicDir = path.join(__dirname, 'public'); if (fs.existsSync(publicDir)) { app.use(express.static(publicDir)); } // Authentication Middleware function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) return res.status(401).json({ error: 'Access denied. No token provided.' }); jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid or expired token.' }); req.user = user; next(); }); } // ========================================== // API ROUTES // ========================================== // 1. Auth Endpoint app.post('/api/auth/login', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); } if (username === ADMIN_USER && bcrypt.compareSync(password, ADMIN_PASS_HASH)) { const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '24h' }); return res.json({ token, username }); } return res.status(401).json({ error: 'Incorrect username or password' }); }); // 2. Departments Endpoints app.get('/api/departments', async (req, res) => { try { const rows = await db.all('SELECT * FROM departments ORDER BY name ASC'); res.json(rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/departments', authenticateToken, async (req, res) => { const { name } = req.body; if (!name || name.trim() === '') { return res.status(400).json({ error: 'Department name is required' }); } try { const result = await db.run('INSERT INTO departments (name) VALUES (?)', [name.trim()]); res.status(201).json({ id: result.id, name: name.trim() }); } catch (error) { if (error.message.includes('UNIQUE')) { return res.status(400).json({ error: 'Department already exists' }); } res.status(500).json({ error: error.message }); } }); app.delete('/api/departments/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { await db.run('UPDATE employees SET department_id = NULL WHERE department_id = ?', [id]); const result = await db.run('DELETE FROM departments WHERE id = ?', [id]); res.json({ success: true, changes: result.changes }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 3. Companies Endpoints (New dictionary CRUD) app.get('/api/companies', async (req, res) => { try { const rows = await db.all('SELECT * FROM companies ORDER BY name ASC'); res.json(rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/companies', authenticateToken, async (req, res) => { const { name } = req.body; if (!name || name.trim() === '') { return res.status(400).json({ error: 'Company name is required' }); } try { const result = await db.run('INSERT INTO companies (name) VALUES (?)', [name.trim()]); res.status(201).json({ id: result.id, name: name.trim() }); } catch (error) { if (error.message.includes('UNIQUE')) { return res.status(400).json({ error: 'Company already exists' }); } res.status(500).json({ error: error.message }); } }); app.delete('/api/companies/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { await db.run('UPDATE employees SET company_id = NULL WHERE company_id = ?', [id]); const result = await db.run('DELETE FROM companies WHERE id = ?', [id]); res.json({ success: true, changes: result.changes }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 4. Employees Endpoints (Search & List) app.get('/api/employees', async (req, res) => { const { search, departmentId } = req.query; let sql = ` SELECT e.*, d.name as department_name, c.name as company_name FROM employees e LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN companies c ON e.company_id = c.id `; const params = []; const conditions = []; if (departmentId) { conditions.push('e.department_id = ?'); params.push(departmentId); } if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); } sql += ' ORDER BY e.name ASC'; try { let rows = await db.all(sql, params); if (search && search.trim() !== '') { const cleanSearch = search.trim().toLowerCase(); const normalizePhone = (p) => { if (!p) return ''; const digits = p.replace(/\D/g, ''); if (digits.length === 11 && (digits.startsWith('7') || digits.startsWith('8'))) { return digits.slice(1); } return digits; }; const searchDigits = normalizePhone(cleanSearch); rows = rows.filter(e => { const nameMatch = e.name && e.name.toLowerCase().includes(cleanSearch); const jobMatch = e.job_title && e.job_title.toLowerCase().includes(cleanSearch); const emailMatch = e.email && e.email.toLowerCase().includes(cleanSearch); const deptMatch = e.department_name && e.department_name.toLowerCase().includes(cleanSearch); const compMatch = e.company_name && e.company_name.toLowerCase().includes(cleanSearch); const buildMatch = e.building && e.building.toLowerCase().includes(cleanSearch); const floorMatch = e.floor && e.floor.toLowerCase().includes(cleanSearch); let phoneMatch = false; if (searchDigits.length > 0 && e.phone) { const empPhoneDigits = normalizePhone(e.phone); if (empPhoneDigits.includes(searchDigits)) { phoneMatch = true; } } return nameMatch || jobMatch || emailMatch || deptMatch || compMatch || buildMatch || floorMatch || phoneMatch; }); } res.json(rows); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create Employee app.post('/api/employees', authenticateToken, async (req, res) => { const { name, company_id, job_title, department_id, phone, email, building, floor } = req.body; if (!name || !job_title) { return res.status(400).json({ error: 'Name and Job Title are required fields.' }); } try { const result = await db.run(` INSERT INTO employees (name, company_id, job_title, department_id, phone, email, building, floor) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ name.trim(), company_id || null, job_title.trim(), department_id || null, phone ? phone.trim() : '', email ? email.trim() : '', building ? building.trim() : '', floor ? floor.trim() : '' ]); const newEmp = await db.get(` SELECT e.*, d.name as department_name, c.name as company_name FROM employees e LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN companies c ON e.company_id = c.id WHERE e.id = ? `, [result.id]); res.status(201).json(newEmp); } catch (error) { res.status(500).json({ error: error.message }); } }); // Update Employee app.put('/api/employees/:id', authenticateToken, async (req, res) => { const { id } = req.params; const { name, company_id, job_title, department_id, phone, email, building, floor } = req.body; if (!name || !job_title) { return res.status(400).json({ error: 'Name and Job Title are required fields.' }); } try { await db.run(` UPDATE employees SET name = ?, company_id = ?, job_title = ?, department_id = ?, phone = ?, email = ?, building = ?, floor = ? WHERE id = ? `, [ name.trim(), company_id || null, job_title.trim(), department_id || null, phone ? phone.trim() : '', email ? email.trim() : '', building ? building.trim() : '', floor ? floor.trim() : '', id ]); const updatedEmp = await db.get(` SELECT e.*, d.name as department_name, c.name as company_name FROM employees e LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN companies c ON e.company_id = c.id WHERE e.id = ? `, [id]); res.json(updatedEmp); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete Employee app.delete('/api/employees/:id', authenticateToken, async (req, res) => { const { id } = req.params; try { const result = await db.run('DELETE FROM employees WHERE id = ?', [id]); res.json({ success: true, changes: result.changes }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Bulk Export (JSON dump of departments, companies, and employees) app.get('/api/admin/export', authenticateToken, async (req, res) => { try { const employees = await db.all('SELECT * FROM employees'); const departments = await db.all('SELECT * FROM departments'); const companies = await db.all('SELECT * FROM companies'); res.json({ employees, departments, companies }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Bulk Import app.post('/api/admin/import', authenticateToken, async (req, res) => { const { employees, departments, companies } = req.body; if (!Array.isArray(employees) || !Array.isArray(departments) || !Array.isArray(companies)) { return res.status(400).json({ error: 'Invalid data format. Expected arrays for employees, departments, and companies.' }); } try { // Truncate existing data await db.run('DELETE FROM employees'); await db.run('DELETE FROM departments'); await db.run('DELETE FROM companies'); // Reset autoincrement sequence await db.run("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'employees'"); await db.run("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'departments'"); await db.run("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'companies'"); // Insert departments const deptIdMapping = {}; for (const d of departments) { const result = await db.run('INSERT INTO departments (name) VALUES (?)', [d.name]); deptIdMapping[d.id] = result.id; } // Insert companies const compIdMapping = {}; for (const c of companies) { const result = await db.run('INSERT INTO companies (name) VALUES (?)', [c.name]); compIdMapping[c.id] = result.id; } // Insert employees for (const e of employees) { const newDeptId = e.department_id ? (deptIdMapping[e.department_id] || null) : null; const newCompId = e.company_id ? (compIdMapping[e.company_id] || null) : null; await db.run(` INSERT INTO employees (name, company_id, job_title, department_id, phone, email, building, floor) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ e.name, newCompId, e.job_title, newDeptId, e.phone || '', e.email || '', e.building || '', e.floor || '' ]); } res.json({ success: true, message: `Imported ${departments.length} departments, ${companies.length} companies and ${employees.length} employees.` }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Catch-all route to serve Index.html if (fs.existsSync(publicDir)) { app.get('*', (req, res) => { res.sendFile(path.join(publicDir, 'index.html')); }); } // Start Server app.listen(PORT, '0.0.0.0', () => { console.log(`Server is running on http://localhost:${PORT}`); console.log(`Ready for Intranet users!`); });