const express = require('express'); const router = express.Router(); const db = require('../../db'); const { isAuthenticated, hasRole } = require('../../middleware/authMiddleware'); const { requireModule } = require('../../middleware/licenseMiddleware'); const cryptoUtil = require('../../utils/cryptoUtil'); // Get all cameras - Protected by Module License router.get('/', requireModule('cctv'), async (req, res) => { try { const [rows] = await db.query('SELECT * FROM cctv_settings ORDER BY display_order ASC, created_at DESC'); // Decrypt usernames and passwords for the UI const cameras = rows.map(cam => ({ ...cam, username: cam.username ? cryptoUtil.decryptMasterKey(cam.username) : cam.username, password: cam.password ? cryptoUtil.decryptMasterKey(cam.password) : cam.password })); res.json(cameras); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); // Reorder cameras (Admin only) router.put('/reorder', hasRole('admin'), async (req, res) => { const { cameras } = req.body; // Array of { id, display_order } or just ordered IDs if (!Array.isArray(cameras)) { return res.status(400).json({ error: 'Invalid data format' }); } try { // Use a transaction for safety await db.query('START TRANSACTION'); for (let i = 0; i < cameras.length; i++) { const cam = cameras[i]; // If receiving array of IDs, use index as order const id = typeof cam === 'object' ? cam.id : cam; const order = i; await db.query('UPDATE cctv_settings SET display_order = ? WHERE id = ?', [order, id]); } await db.query('COMMIT'); res.json({ message: 'Cameras reordered' }); } catch (err) { await db.query('ROLLBACK'); console.error(err); res.status(500).json({ error: 'Database error' }); } }); // --- Zone Management --- // Get all zones router.get('/zones', isAuthenticated, async (req, res) => { try { const [rows] = await db.query('SELECT * FROM cctv_zones ORDER BY display_order ASC, name ASC'); res.json(rows); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); // Update zones (Sync list) router.put('/zones', hasRole('admin'), async (req, res) => { const { zones } = req.body; if (!Array.isArray(zones)) return res.status(400).json({ error: 'Invalid data' }); try { await db.query('START TRANSACTION'); // Rebuild zone table for simplicity since we link by name string in cctv_settings await db.query('DELETE FROM cctv_zones'); for (let i = 0; i < zones.length; i++) { const z = zones[i]; const name = typeof z === 'string' ? z : z.name; const layout = typeof z === 'object' ? (z.layout || '2*2') : '2*2'; if (name) { await db.query('INSERT INTO cctv_zones (name, layout, display_order) VALUES (?, ?, ?)', [name, layout, i]); } } await db.query('COMMIT'); res.json({ message: 'Zones updated' }); } catch (err) { await db.query('ROLLBACK'); console.error(err); res.status(500).json({ error: 'Failed to update zones' }); } }); // Update specific zone layout (Real-time override) router.patch('/zones/:name/layout', hasRole('admin'), async (req, res) => { const { layout } = req.body; try { const [result] = await db.query('UPDATE cctv_zones SET layout = ? WHERE name = ?', [layout, req.params.name]); if (result.affectedRows === 0) return res.status(404).json({ error: 'Zone not found' }); res.json({ success: true, message: 'Layout updated' }); } catch (err) { console.error(err); res.status(500).json({ error: 'Failed to update layout' }); } }); // Get single camera router.get('/:id', async (req, res) => { try { const [rows] = await db.query('SELECT * FROM cctv_settings WHERE id = ?', [req.params.id]); if (rows.length === 0) return res.status(404).json({ error: 'Camera not found' }); const camera = rows[0]; camera.username = camera.username ? cryptoUtil.decryptMasterKey(camera.username) : camera.username; camera.password = camera.password ? cryptoUtil.decryptMasterKey(camera.password) : camera.password; res.json(camera); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); // Add camera (Admin only) router.post('/', hasRole('admin'), async (req, res) => { const { name, zone, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality } = req.body; try { const encryptedUser = username ? cryptoUtil.encryptMasterKey(username) : username; const encryptedPass = password ? cryptoUtil.encryptMasterKey(password) : password; const sql = `INSERT INTO cctv_settings (name, zone, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const [result] = await db.query(sql, [name, zone || '기본 구역', ip_address, port || 554, encryptedUser, encryptedPass, stream_path || '/stream1', transport_mode || 'tcp', rtsp_encoding || false, quality || 'low']); res.status(201).json({ message: 'Camera added', id: result.insertId }); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); // Update camera (Admin only) router.put('/:id', hasRole('admin'), async (req, res) => { const { name, zone, ip_address, port, username, password, stream_path, transport_mode, rtsp_encoding, quality } = req.body; try { const encryptedUser = username ? cryptoUtil.encryptMasterKey(username) : username; const encryptedPass = password ? cryptoUtil.encryptMasterKey(password) : password; const sql = `UPDATE cctv_settings SET name=?, zone=?, ip_address=?, port=?, username=?, password=?, stream_path=?, transport_mode=?, rtsp_encoding=?, quality=? WHERE id=?`; const [result] = await db.query(sql, [name, zone, ip_address, port, encryptedUser, encryptedPass, stream_path, transport_mode, rtsp_encoding, quality, req.params.id]); if (result.affectedRows === 0) return res.status(404).json({ error: 'Camera not found' }); // Force stream reset (kick clients to trigger reconnect) const streamRelay = req.app.get('streamRelay'); if (streamRelay) { console.log(`Settings changed for camera ${req.params.id}, resetting stream...`); streamRelay.resetStream(req.params.id); } res.json({ message: 'Camera updated' }); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); // Toggle Stream Status (Admin only) router.patch('/:id/status', hasRole('admin'), async (req, res) => { const { is_active } = req.body; try { const [result] = await db.query('UPDATE cctv_settings SET is_active = ? WHERE id = ?', [is_active, req.params.id]); if (result.affectedRows === 0) return res.status(404).json({ error: 'Camera not found' }); const streamRelay = req.app.get('streamRelay'); if (streamRelay) { // If disabled, stop stream. If enabled, reset (which stops and allows reconnect) console.log(`Stream status changed for camera ${req.params.id} to ${is_active}`); streamRelay.resetStream(req.params.id); } res.json({ message: `Stream ${is_active ? 'enabled' : 'disabled'}` }); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); // Delete camera (Admin only) router.delete('/:id', hasRole('admin'), async (req, res) => { try { const [result] = await db.query('DELETE FROM cctv_settings WHERE id = ?', [req.params.id]); if (result.affectedRows === 0) return res.status(404).json({ error: 'Camera not found' }); // Stop stream const streamRelay = req.app.get('streamRelay'); if (streamRelay) { streamRelay.stopStream(req.params.id); } res.json({ message: 'Camera deleted' }); } catch (err) { console.error(err); res.status(500).json({ error: 'Database error' }); } }); const { exec } = require('child_process'); // ... existing routes ... // 7. Ping Test (Troubleshooting) router.get('/:id/ping', isAuthenticated, async (req, res) => { try { const [rows] = await db.query('SELECT ip_address FROM cctv_settings WHERE id = ?', [req.params.id]); if (rows.length === 0) return res.status(404).json({ error: 'Camera not found' }); const ip = rows[0].ip_address; // Simple ping command const platform = process.platform; const cmd = platform === 'win32' ? `ping -n 1 ${ip}` : `ping -c 1 ${ip}`; exec(cmd, (error, stdout, stderr) => { res.json({ ip, success: !error, output: stdout, error: stderr }); }); } catch (err) { res.status(500).json({ error: 'Ping failed' }); } }); module.exports = router;