240 lines
9.2 KiB
JavaScript

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;