smart_ims/server/index.js
2026-01-22 23:42:55 +09:00

507 lines
21 KiB
JavaScript

const express = require('express');
const cors = require('cors');
require('dotenv').config();
const db = require('./db');
const authRoutes = require('./routes/auth');
const cameraRoutes = require('./modules/cctv/routes');
const StreamRelay = require('./modules/cctv/streamRelay');
const { csrfProtection } = require('./middleware/csrfMiddleware');
const { isAuthenticated } = require('./middleware/authMiddleware');
const app = express();
const PORT = process.env.PORT || 3005; // Changed to 3005 to avoid conflict with Synology services (3001 issue)
const path = require('path');
const fs = require('fs');
// Ensure uploads directory exists
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
// Multer Configuration
const multer = require('multer');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
// Use timestamp + random number + extension to avoid encoding issues with Korean filenames
const ext = path.extname(file.originalname);
const filename = `${Date.now()}-${Math.round(Math.random() * 1000)}${ext}`;
cb(null, filename);
}
});
const upload = multer({ storage: storage });
// Session Store Configuration
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const sessionStoreOptions = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
clearExpired: true,
checkExpirationInterval: 900000, // 15 min
expiration: 86400000 // 1 day
};
const sessionStore = new MySQLStore(sessionStoreOptions);
// Middleware
app.use(cors({
origin: true, // Allow all origins (or specific one)
credentials: true // Important for cookies
}));
app.use(express.json());
app.use('/uploads', express.static(uploadDir));
// Session Middleware
app.use(session({
key: 'smartasset_sid',
secret: process.env.SESSION_SECRET || 'smartasset_session_secret_key',
store: sessionStore,
resave: false,
saveUninitialized: true, // Save new sessions even if empty (helps with some client handshake issues)
cookie: {
httpOnly: true, // Prevent JS access
secure: false, // Set true if using HTTPS
maxAge: 1000 * 60 * 60 * 24, // 1 day
sameSite: 'lax' // Recommended for better CSRF protection and reliability
}
}));
// Apply CSRF Protection
app.use(csrfProtection);
// Request Logger
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// Test DB Connection
db.query('SELECT 1')
.then(() => console.log('✅ Connected to Database'))
.catch(err => {
console.error('❌ Database Connection Failed:', err.message);
console.error('Hint: Check your .env DB_HOST and ensure Synology MariaDB allows remote connections.');
});
// Ensure Tables Exist
const initTables = async () => {
try {
const assetTable = `
CREATE TABLE IF NOT EXISTS assets (
id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL,
image_url VARCHAR(255),
model_name VARCHAR(100),
serial_number VARCHAR(100),
manufacturer VARCHAR(100),
location VARCHAR(100),
purchase_date DATE,
purchase_price BIGINT,
manager VARCHAR(50),
status ENUM('active', 'maintain', 'broken', 'disposed') DEFAULT 'active',
calibration_cycle VARCHAR(50),
specs TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
const maintenanceTable = `
CREATE TABLE IF NOT EXISTS maintenance_history (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(20) NOT NULL,
maintenance_date DATE NOT NULL,
type VARCHAR(50) NOT NULL,
content TEXT,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(assetTable);
await db.query(maintenanceTable);
const [usersTableExists] = await db.query("SHOW TABLES LIKE 'users'");
if (usersTableExists.length === 0) {
const usersTableSQL = `
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(50) PRIMARY KEY,
password VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
department VARCHAR(100),
position VARCHAR(100),
phone VARCHAR(255),
role ENUM('admin', 'user') DEFAULT 'user',
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(usersTableSQL);
console.log('✅ Users Table Created');
// Default Admin
const adminId = 'admin';
const [adminExists] = await db.query('SELECT 1 FROM users WHERE id = ?', [adminId]);
if (adminExists.length === 0) {
const crypto = require('crypto');
const hashedPass = crypto.createHash('sha256').update('admin123').digest('hex');
await db.query(
'INSERT INTO users (id, password, name, role, department, position) VALUES (?, ?, ?, ?, ?, ?)',
[adminId, hashedPass, '시스템 관리자', 'admin', 'IT팀', '관리자']
);
console.log('✅ Default Admin Created (admin / admin123)');
}
}
console.log('✅ Tables Initialized');
// Create asset_manuals table
const manualTable = `
CREATE TABLE IF NOT EXISTS asset_manuals (
id INT AUTO_INCREMENT PRIMARY KEY,
asset_id VARCHAR(20) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_url VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(manualTable);
// Create camera_settings table
const cameraTable = `
CREATE TABLE IF NOT EXISTS camera_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
ip_address VARCHAR(100) NOT NULL,
port INT DEFAULT 554,
username VARCHAR(100),
password VARCHAR(100),
stream_path VARCHAR(200) DEFAULT '/stream1',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(cameraTable);
// Check for 'transport_mode' and 'rtsp_encoding' columns and add if missing
const [camColumns] = await db.query("SHOW COLUMNS FROM camera_settings LIKE 'transport_mode'");
if (camColumns.length === 0) {
await db.query("ALTER TABLE camera_settings ADD COLUMN transport_mode ENUM('tcp', 'udp', 'auto') DEFAULT 'tcp' AFTER stream_path");
await db.query("ALTER TABLE camera_settings ADD COLUMN rtsp_encoding BOOLEAN DEFAULT FALSE AFTER transport_mode");
await db.query("ALTER TABLE camera_settings ADD COLUMN quality ENUM('low', 'medium', 'original') DEFAULT 'low' AFTER transport_mode"); // Default to low for stability
console.log("✅ Added 'transport_mode', 'quality', and 'rtsp_encoding' columns to camera_settings");
} else {
// Check if quality exists (for subsequent updates)
const [qualCol] = await db.query("SHOW COLUMNS FROM camera_settings LIKE 'quality'");
if (qualCol.length === 0) {
await db.query("ALTER TABLE camera_settings ADD COLUMN quality ENUM('low', 'medium', 'original') DEFAULT 'low' AFTER transport_mode");
console.log("✅ Added 'quality' column to camera_settings");
}
}
// Check for 'display_order' column
const [orderCol] = await db.query("SHOW COLUMNS FROM camera_settings LIKE 'display_order'");
if (orderCol.length === 0) {
await db.query("ALTER TABLE camera_settings ADD COLUMN display_order INT DEFAULT 0 AFTER quality");
console.log("✅ Added 'display_order' column to camera_settings");
}
// Create system_settings table (Key-Value store)
const systemSettingsTable = `
CREATE TABLE IF NOT EXISTS system_settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(systemSettingsTable);
// Create system_modules table
const systemModulesTable = `
CREATE TABLE IF NOT EXISTS system_modules (
code VARCHAR(50) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT FALSE,
license_key TEXT,
license_type VARCHAR(20),
expiry_date DATETIME,
subscriber_id VARCHAR(50),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(systemModulesTable);
// Create license_history table
const licenseHistoryTable = `
CREATE TABLE IF NOT EXISTS license_history (
id INT AUTO_INCREMENT PRIMARY KEY,
module_code VARCHAR(50) NOT NULL,
license_key TEXT,
license_type VARCHAR(20),
subscriber_id VARCHAR(50),
activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
replaced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(licenseHistoryTable);
// Create issued_licenses table (for tracking generated keys)
const issuedLicensesTable = `
CREATE TABLE IF NOT EXISTS issued_licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
module_code VARCHAR(50) NOT NULL,
license_key TEXT,
license_type VARCHAR(20),
subscriber_id VARCHAR(50),
status VARCHAR(20) DEFAULT 'ISSUED', -- ISSUED, ACTIVE, EXPIRED, REVOKED
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
activation_deadline DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await db.query(issuedLicensesTable);
// Check for 'subscriber_id' column in system_modules (for existing installations)
const [modColumns] = await db.query("SHOW COLUMNS FROM system_modules LIKE 'subscriber_id'");
if (modColumns.length === 0) {
await db.query("ALTER TABLE system_modules ADD COLUMN subscriber_id VARCHAR(50) AFTER expiry_date");
console.log("✅ Added 'subscriber_id' column to system_modules");
}
// Initialize default modules if empty (Disabled by default as per request behavior)
const [existingModules] = await db.query('SELECT 1 FROM system_modules LIMIT 1');
if (existingModules.length === 0) {
const insert = `INSERT INTO system_modules (code, name, is_active, license_type) VALUES (?, ?, ?, ?)`;
await db.query(insert, ['asset', '자산 관리', true, 'dev']);
await db.query(insert, ['production', '생산 관리', false, null]);
await db.query(insert, ['monitoring', 'CCTV', false, null]);
}
console.log('✅ Tables Initialized');
} catch (err) {
console.error('❌ Table Initialization Failed:', err);
}
};
initTables();
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', version: '1.2.0', timestamp: '2026-01-22 21:18' });
});
// Routes
app.use('/api', authRoutes);
app.use('/api/cameras', cameraRoutes);
app.use('/api/system', require('./routes/system'));
// Protect following routes
app.use(['/api/assets', '/api/maintenance', '/api/manuals', '/api/upload'], isAuthenticated);
// 0. File Upload Endpoint
app.post('/api/upload', upload.single('image'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Return the full URL or relative path
// Assuming server is accessed via same host, we just return the path
const fileUrl = `/uploads/${req.file.filename}`;
res.json({ url: fileUrl });
});
// 1. Get All Assets
app.get('/api/assets', async (req, res) => {
try {
const [rows] = await db.query('SELECT * FROM assets ORDER BY created_at DESC');
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 2. Get Asset by ID
app.get('/api/assets/:id', async (req, res) => {
try {
const [rows] = await db.query('SELECT * FROM assets WHERE id = ?', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 3. Create Asset
app.post('/api/assets', async (req, res) => {
const { id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url } = req.body;
try {
const sql = `
INSERT INTO assets (id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
await db.query(sql, [id, name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url]);
res.status(201).json({ message: 'Asset created', id });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 4. Update Asset
app.put('/api/assets/:id', async (req, res) => {
const { name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url } = req.body;
try {
const sql = `
UPDATE assets
SET name=?, category=?, model_name=?, serial_number=?, manufacturer=?, location=?, purchase_date=?, manager=?, status=?, specs=?, purchase_price=?, image_url=?
WHERE id=?
`;
const [result] = await db.query(sql, [name, category, model_name, serial_number, manufacturer, location, purchase_date, manager, status, specs, purchase_price, image_url, req.params.id]);
if (result.affectedRows === 0) return res.status(404).json({ error: 'Asset not found' });
res.json({ message: 'Asset updated' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 5. Get All Maintenance Records for an Asset
app.get('/api/assets/:asset_id/maintenance', async (req, res) => {
try {
const [rows] = await db.query('SELECT * FROM maintenance_history WHERE asset_id = ? ORDER BY maintenance_date DESC', [req.params.asset_id]);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 6. Get Single Maintenance Record by ID
app.get('/api/maintenance/:id', async (req, res) => {
try {
const [rows] = await db.query('SELECT * FROM maintenance_history WHERE id = ?', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Maintenance record not found' });
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 7. Create Maintenance Record
app.post('/api/assets/:asset_id/maintenance', async (req, res) => {
const { maintenance_date, type, content, images } = req.body;
const { asset_id } = req.params;
try {
const sql = `
INSERT INTO maintenance_history (asset_id, maintenance_date, type, content, images)
VALUES (?, ?, ?, ?, ?)
`;
const [result] = await db.query(sql, [asset_id, maintenance_date, type, content, JSON.stringify(images || [])]);
res.status(201).json({ message: 'Maintenance record created', id: result.insertId });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 8. Update Maintenance Record
app.put('/api/maintenance/:id', async (req, res) => {
const { maintenance_date, type, content, images } = req.body;
try {
const sql = `
UPDATE maintenance_history
SET maintenance_date=?, type=?, content=?, images=?
WHERE id=?
`;
const [result] = await db.query(sql, [maintenance_date, type, content, JSON.stringify(images || []), req.params.id]);
if (result.affectedRows === 0) return res.status(404).json({ error: 'Maintenance record not found' });
res.json({ message: 'Maintenance record updated' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 9. Delete Maintenance Record
app.delete('/api/maintenance/:id', async (req, res) => {
try {
const [result] = await db.query('DELETE FROM maintenance_history WHERE id = ?', [req.params.id]);
if (result.affectedRows === 0) return res.status(404).json({ error: 'Maintenance record not found' });
res.json({ message: 'Maintenance record deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 10. Get Manuals for an Asset
app.get('/api/assets/:asset_id/manuals', async (req, res) => {
try {
const [rows] = await db.query('SELECT * FROM asset_manuals WHERE asset_id = ? ORDER BY created_at DESC', [req.params.asset_id]);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 11. Add Manual to Asset
app.post('/api/assets/:asset_id/manuals', async (req, res) => {
const { file_name, file_url } = req.body;
const { asset_id } = req.params;
try {
const sql = `INSERT INTO asset_manuals (asset_id, file_name, file_url) VALUES (?, ?, ?)`;
const [result] = await db.query(sql, [asset_id, file_name, file_url]);
res.status(201).json({ message: 'Manual added', id: result.insertId });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// 12. Delete Manual
app.delete('/api/manuals/:id', async (req, res) => {
try {
const [result] = await db.query('DELETE FROM asset_manuals WHERE id = ?', [req.params.id]);
if (result.affectedRows === 0) return res.status(404).json({ error: 'Manual not found' });
res.json({ message: 'Manual deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// Serve Frontend Static Files (Production/Deployment)
app.use(express.static(path.join(__dirname, '../dist')));
// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get(/(.*)/, (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Initialize Stream Relay
const streamRelay = new StreamRelay(server);
app.set('streamRelay', streamRelay);