558 lines
24 KiB
JavaScript
558 lines
24 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);
|
|
sessionStore.on('error', (err) => {
|
|
console.error('Session Store Error:', err);
|
|
});
|
|
|
|
// Middleware
|
|
app.use(cors({
|
|
origin: true,
|
|
credentials: true,
|
|
allowedHeaders: ['Content-Type', 'X-CSRF-Token', 'Authorization']
|
|
}));
|
|
app.use(express.json());
|
|
app.use('/uploads', express.static(uploadDir));
|
|
|
|
// Session Middleware
|
|
app.use(session({
|
|
key: 'smartims_sid',
|
|
secret: process.env.SESSION_SECRET || 'smartims_session_secret_key',
|
|
store: sessionStore,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
rolling: false, // Do not automatic rolling (we control it in middleware)
|
|
cookie: {
|
|
httpOnly: true,
|
|
secure: false, // HTTPS 사용 시 true로 변경 필요
|
|
maxAge: 3600000, // 기본 1시간 (미들웨어에서 동적 조정)
|
|
sameSite: 'lax'
|
|
}
|
|
}));
|
|
|
|
// Dynamic Session Timeout Middleware
|
|
app.use(async (req, res, next) => {
|
|
if (req.session && req.session.user) {
|
|
// Skip session extension for background check requests
|
|
if (req.path === '/api/check' || req.path === '/check' || req.path.includes('/auth/check')) {
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
// Priority: User's individual timeout > System default
|
|
let timeoutMinutes = 60; // Default fallback
|
|
|
|
if (req.session.user.session_timeout) {
|
|
timeoutMinutes = parseInt(req.session.user.session_timeout);
|
|
} else {
|
|
const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'session_timeout'");
|
|
timeoutMinutes = rows.length > 0 ? parseInt(rows[0].setting_value) : 10; // New default fallback 10 as requested
|
|
}
|
|
|
|
req.session.cookie.maxAge = timeoutMinutes * 60 * 1000;
|
|
|
|
// Explicitly save session before moving to next middleware
|
|
req.session.save((err) => {
|
|
if (err) console.error('Session save error:', err);
|
|
next();
|
|
});
|
|
return;
|
|
} catch (err) {
|
|
console.error('Session timeout fetch error:', err);
|
|
}
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Apply CSRF Protection
|
|
app.use(csrfProtection);
|
|
|
|
// Request Logger with Session Remaining Time
|
|
app.use((req, res, next) => {
|
|
const now = new Date();
|
|
const kstDate = new Date(now.getTime() + (9 * 60 * 60 * 1000)).toISOString().replace('Z', '').replace('T', ' ');
|
|
|
|
let sessionInfo = '';
|
|
if (req.session && req.session.cookie && req.session.cookie.expires) {
|
|
const remainingMs = req.session.cookie.expires - now;
|
|
if (remainingMs > 0) {
|
|
const remMin = Math.floor(remainingMs / 60000);
|
|
const remSec = Math.floor((remainingMs % 60000) / 1000);
|
|
sessionInfo = ` [Session: ${remMin}m ${remSec}s left]`;
|
|
} else {
|
|
sessionInfo = ` [Session: Expired]`;
|
|
}
|
|
}
|
|
|
|
console.log(`[${kstDate}]${sessionInfo} ${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(assetTable);
|
|
await db.query(maintenanceTable);
|
|
|
|
// Check/Add 'quantity' column to assets
|
|
const [assetCols] = await db.query("SHOW COLUMNS FROM assets LIKE 'quantity'");
|
|
if (assetCols.length === 0) {
|
|
await db.query("ALTER TABLE assets ADD COLUMN quantity INT DEFAULT 1");
|
|
console.log("✅ Added 'quantity' column to assets");
|
|
}
|
|
|
|
// Check/Add 'parent_id' column to assets for sub-equipment management
|
|
const [parentIdCols] = await db.query("SHOW COLUMNS FROM assets LIKE 'parent_id'");
|
|
if (parentIdCols.length === 0) {
|
|
await db.query("ALTER TABLE assets ADD COLUMN parent_id VARCHAR(20) AFTER id");
|
|
await db.query("ALTER TABLE assets ADD CONSTRAINT fk_assets_parent FOREIGN KEY (parent_id) REFERENCES assets(id) ON DELETE SET NULL");
|
|
console.log("✅ Added 'parent_id' column to assets");
|
|
}
|
|
|
|
// Create maintenance_parts table
|
|
const maintenancePartsTable = `
|
|
CREATE TABLE IF NOT EXISTS maintenance_parts (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
maintenance_id INT NOT NULL,
|
|
part_id VARCHAR(20) NOT NULL,
|
|
quantity INT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (maintenance_id) REFERENCES maintenance_history(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (part_id) REFERENCES assets(id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
`;
|
|
await db.query(maintenancePartsTable);
|
|
|
|
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('supervisor', 'admin', 'user') DEFAULT 'user',
|
|
session_timeout INT DEFAULT 10,
|
|
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, '관리자', 'supervisor', 'IT팀', '관리자']
|
|
);
|
|
console.log('✅ Default Admin Created as Supervisor');
|
|
}
|
|
}
|
|
|
|
// 2. Ensure schema updates for existing table
|
|
try {
|
|
await db.query("ALTER TABLE users MODIFY COLUMN role ENUM('supervisor', 'admin', 'user') DEFAULT 'user'");
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
|
|
const [userTimeoutCols] = await db.query("SHOW COLUMNS FROM users LIKE 'session_timeout'");
|
|
if (userTimeoutCols.length === 0) {
|
|
await db.query("ALTER TABLE users ADD COLUMN session_timeout INT DEFAULT 10 AFTER role");
|
|
console.log("✅ Added 'session_timeout' column to users");
|
|
}
|
|
|
|
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 asset_accessories table
|
|
const accessoryTable = `
|
|
CREATE TABLE IF NOT EXISTS asset_accessories (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
asset_id VARCHAR(20) NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
spec VARCHAR(100),
|
|
quantity INT DEFAULT 1,
|
|
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(accessoryTable);
|
|
|
|
// 1. Rename camera_settings to cctv_settings if old table exists
|
|
const [oldCamTable] = await db.query("SHOW TABLES LIKE 'camera_settings'");
|
|
if (oldCamTable.length > 0) {
|
|
await db.query("RENAME TABLE camera_settings TO cctv_settings");
|
|
console.log("✅ Renamed 'camera_settings' to 'cctv_settings'");
|
|
}
|
|
|
|
// Create cctv_settings table
|
|
const cameraTable = `
|
|
CREATE TABLE IF NOT EXISTS cctv_settings (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
zone VARCHAR(50) DEFAULT '기본 구역',
|
|
ip_address VARCHAR(100) NOT NULL,
|
|
port INT DEFAULT 554,
|
|
username VARCHAR(100),
|
|
password VARCHAR(255),
|
|
stream_path VARCHAR(200) DEFAULT '/stream1',
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
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);
|
|
|
|
// Ensure password field is long enough for encryption
|
|
await db.query("ALTER TABLE cctv_settings MODIFY COLUMN password VARCHAR(255)");
|
|
|
|
// Check for 'transport_mode' and 'rtsp_encoding' columns and add if missing
|
|
const [camColumns] = await db.query("SHOW COLUMNS FROM cctv_settings LIKE 'transport_mode'");
|
|
if (camColumns.length === 0) {
|
|
await db.query("ALTER TABLE cctv_settings ADD COLUMN transport_mode ENUM('tcp', 'udp', 'auto') DEFAULT 'tcp' AFTER stream_path");
|
|
await db.query("ALTER TABLE cctv_settings ADD COLUMN rtsp_encoding BOOLEAN DEFAULT FALSE AFTER transport_mode");
|
|
await db.query("ALTER TABLE cctv_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 cctv_settings");
|
|
} else {
|
|
// Check if quality exists (for subsequent updates)
|
|
const [qualCol] = await db.query("SHOW COLUMNS FROM cctv_settings LIKE 'quality'");
|
|
if (qualCol.length === 0) {
|
|
await db.query("ALTER TABLE cctv_settings ADD COLUMN quality ENUM('low', 'medium', 'original') DEFAULT 'low' AFTER transport_mode");
|
|
console.log("✅ Added 'quality' column to cctv_settings");
|
|
}
|
|
}
|
|
|
|
// Check for 'zone' column
|
|
const [zoneCol] = await db.query("SHOW COLUMNS FROM cctv_settings LIKE 'zone'");
|
|
if (zoneCol.length === 0) {
|
|
await db.query("ALTER TABLE cctv_settings ADD COLUMN zone VARCHAR(50) DEFAULT '기본 구역' AFTER name");
|
|
console.log("✅ Added 'zone' column to cctv_settings");
|
|
}
|
|
|
|
// Check for 'display_order' column
|
|
const [orderCol] = await db.query("SHOW COLUMNS FROM cctv_settings LIKE 'display_order'");
|
|
if (orderCol.length === 0) {
|
|
await db.query("ALTER TABLE cctv_settings ADD COLUMN display_order INT DEFAULT 0 AFTER quality");
|
|
console.log("✅ Added 'display_order' column to cctv_settings");
|
|
}
|
|
|
|
// Create cctv_zones table
|
|
const zoneTable = `
|
|
CREATE TABLE IF NOT EXISTS cctv_zones (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
name VARCHAR(50) NOT NULL UNIQUE,
|
|
layout ENUM('1', '1*2', '2*2') DEFAULT '2*2',
|
|
display_order INT DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
`;
|
|
await db.query(zoneTable);
|
|
|
|
// Check for 'layout' column (for migration)
|
|
const [layoutCol] = await db.query("SHOW COLUMNS FROM cctv_zones LIKE 'layout'");
|
|
if (layoutCol.length === 0) {
|
|
await db.query("ALTER TABLE cctv_zones ADD COLUMN layout VARCHAR(10) DEFAULT '2*2' AFTER name");
|
|
console.log("✅ Added 'layout' column to cctv_zones");
|
|
} else {
|
|
// Migration: Convert ENUM to VARCHAR
|
|
await db.query("ALTER TABLE cctv_zones MODIFY COLUMN layout VARCHAR(10) DEFAULT '2*2'");
|
|
}
|
|
|
|
// Initialize default zone if empty
|
|
const [existingZones] = await db.query('SELECT 1 FROM cctv_zones LIMIT 1');
|
|
if (existingZones.length === 0) {
|
|
await db.query("INSERT INTO cctv_zones (name, display_order) VALUES ('기본 구역', 0)");
|
|
console.log("✅ Initialized default CCTV zone");
|
|
}
|
|
|
|
// 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', '자산 관리', false, null]);
|
|
await db.query(insert, ['production', '생산 관리', false, null]);
|
|
await db.query(insert, ['cctv', 'CCTV', false, null]);
|
|
} else {
|
|
// One-time update: Rename 'monitoring' code to 'cctv' (migration)
|
|
// Use subquery or check if cctv exists to avoid ER_DUP_ENTRY
|
|
const [cctvExists] = await db.query("SELECT 1 FROM system_modules WHERE code = 'cctv'");
|
|
if (cctvExists.length > 0) {
|
|
// If cctv already exists, just remove monitoring if it's there
|
|
await db.query("DELETE FROM system_modules WHERE code = 'monitoring'");
|
|
} else {
|
|
// If cctv doesn't exist, try renaming monitoring
|
|
await db.query("UPDATE system_modules SET code = 'cctv' WHERE code = 'monitoring'");
|
|
}
|
|
}
|
|
|
|
console.log('✅ Tables Initialized');
|
|
} catch (err) {
|
|
console.error('❌ Table Initialization Failed:', err);
|
|
}
|
|
};
|
|
initTables();
|
|
|
|
const packageJson = require('./package.json');
|
|
|
|
app.get('/api/health', (req, res) => {
|
|
// Dynamic version check (Light-weight)
|
|
const kstOffset = 9 * 60 * 60 * 1000;
|
|
const kstDate = new Date(Date.now() + kstOffset);
|
|
|
|
let version = packageJson.version;
|
|
try {
|
|
const { execSync } = require('child_process');
|
|
// Check git tag in parent directory (Project root)
|
|
version = execSync('git describe --tags --abbrev=0', {
|
|
cwd: path.join(__dirname, '..'),
|
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
}).toString().trim().replace(/^v/, '');
|
|
} catch (e) {
|
|
// Safe fallback to package.json
|
|
}
|
|
|
|
res.json({
|
|
status: 'ok',
|
|
version: version,
|
|
node_version: process.version,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
timestamp: kstDate.toISOString().replace('T', ' ').split('.')[0]
|
|
});
|
|
});
|
|
|
|
// Routes
|
|
app.use('/api', authRoutes);
|
|
app.use('/api/cameras', cameraRoutes);
|
|
app.use('/api/system', require('./routes/system'));
|
|
|
|
const { requireModule } = require('./middleware/licenseMiddleware');
|
|
|
|
// Protect following routes
|
|
app.use(['/api/assets', '/api/maintenance', '/api/manuals', '/api/upload'], isAuthenticated, requireModule('asset'));
|
|
|
|
// 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 (Moved to modules/asset/routes.js)
|
|
// 2. Get Asset by ID (Moved to modules/asset/routes.js)
|
|
// 3. Create Asset (Moved to modules/asset/routes.js)
|
|
// 4. Update Asset (Moved to modules/asset/routes.js)
|
|
// 5. Get All Maintenance Records for an Asset (Moved to modules/asset/routes.js)
|
|
// 6. Get Single Maintenance Record by ID (Moved to modules/asset/routes.js)
|
|
// 7. Create Maintenance Record (Moved to modules/asset/routes.js)
|
|
// 8. Update Maintenance Record (Moved to modules/asset/routes.js)
|
|
// 9. Delete Maintenance Record (Moved to modules/asset/routes.js)
|
|
// 10. Get Manuals for an Asset (Moved to modules/asset/routes.js)
|
|
// 11. Add Manual to Asset (Moved to modules/asset/routes.js)
|
|
// 12. Delete Manual (Moved to modules/asset/routes.js)
|
|
|
|
// Mount Asset Module
|
|
app.use('/api', require('./modules/asset/routes'));
|
|
|
|
// Serve Frontend Static Files (Production/Deployment)
|
|
// Strict Sibling Structure: Expects 'dist' to be a sibling of 'server'
|
|
// This matches the local development environment structure.
|
|
const distPath = path.join(__dirname, '../dist');
|
|
|
|
console.log(`Serving static files from: ${distPath}`);
|
|
app.use(express.static(distPath));
|
|
|
|
// The "catchall" handler: for any request that doesn't
|
|
// match one above, send back React's index.html file.
|
|
app.get(/(.*)/, (req, res) => {
|
|
// Prevent caching for index.html to ensure updates are detected immediately
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
res.sendFile(path.join(distPath, '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);
|