Files
2026-05-25 09:45:08 +00:00

382 lines
12 KiB
JavaScript

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!`);
});