sync rules

This commit is contained in:
sebseb7
2025-12-21 02:28:37 +01:00
parent f8a83efb39
commit 096fc2aa72
6 changed files with 776 additions and 333 deletions

158
server.js
View File

@@ -65,6 +65,20 @@ db.exec(`
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
enabled INTEGER DEFAULT 1,
trigger_type TEXT NOT NULL,
trigger_data TEXT NOT NULL,
action_type TEXT NOT NULL,
action_data TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
const insertStmt = db.prepare(`
INSERT INTO readings (dev_id, dev_name, port, port_name, temp_c, humidity, vpd, fan_speed, on_speed, off_speed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -72,6 +86,19 @@ const insertStmt = db.prepare(`
const getUserByUsername = db.prepare('SELECT * FROM users WHERE username = ?');
// Rules prepared statements
const getAllRules = db.prepare('SELECT * FROM rules ORDER BY id');
const getRuleById = db.prepare('SELECT * FROM rules WHERE id = ?');
const insertRule = db.prepare(`
INSERT INTO rules (name, enabled, trigger_type, trigger_data, action_type, action_data)
VALUES (?, ?, ?, ?, ?, ?)
`);
const updateRule = db.prepare(`
UPDATE rules SET name = ?, enabled = ?, trigger_type = ?, trigger_data = ?, action_type = ?, action_data = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
const deleteRule = db.prepare('DELETE FROM rules WHERE id = ?');
// --- AC INFINITY API LOGIC ---
let token = null;
@@ -269,6 +296,137 @@ app.get('/api/auth/me', (req, res) => {
}
});
// --- AUTH MIDDLEWARE ---
function optionalAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
try {
const token = authHeader.split(' ')[1];
req.user = jwt.verify(token, JWT_SECRET);
} catch (e) {
req.user = null;
}
}
next();
}
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const token = authHeader.split(' ')[1];
req.user = jwt.verify(token, JWT_SECRET);
next();
} catch (e) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
function requireAdmin(req, res, next) {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// --- RULES API ---
// Helper to format rule for API response
function formatRule(row) {
const trigger = JSON.parse(row.trigger_data);
const action = JSON.parse(row.action_data);
// Add type back for legacy/action compatibility
if (action.type === undefined && row.action_type) {
action.type = row.action_type;
}
return {
id: row.id,
name: row.name,
enabled: row.enabled === 1,
trigger,
action
};
}
// GET /api/rules - public (guests can view)
app.get('/api/rules', (req, res) => {
try {
const rows = getAllRules.all();
res.json(rows.map(formatRule));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/rules - admin only
app.post('/api/rules', requireAuth, requireAdmin, (req, res) => {
try {
const { name, enabled, trigger, action } = req.body;
if (!name || !trigger || !action) {
return res.status(400).json({ error: 'name, trigger, and action required' });
}
// Determine trigger type for storage (combined, time, sensor)
let triggerType = 'combined';
if (trigger.timeRange && !trigger.sensors?.length) triggerType = 'time';
if (!trigger.timeRange && trigger.sensors?.length) triggerType = 'sensor';
const triggerData = JSON.stringify(trigger);
const actionData = JSON.stringify(action);
const result = insertRule.run(name, enabled ? 1 : 0, triggerType, triggerData, action.type, actionData);
const newRule = getRuleById.get(result.lastInsertRowid);
res.status(201).json(formatRule(newRule));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/rules/:id - admin only
app.put('/api/rules/:id', requireAuth, requireAdmin, (req, res) => {
try {
const { id } = req.params;
const existing = getRuleById.get(id);
if (!existing) {
return res.status(404).json({ error: 'Rule not found' });
}
const { name, enabled, trigger, action } = req.body;
// Determine trigger type for storage
let triggerType = 'combined';
if (trigger.timeRange && !trigger.sensors?.length) triggerType = 'time';
if (!trigger.timeRange && trigger.sensors?.length) triggerType = 'sensor';
const triggerData = JSON.stringify(trigger);
const actionData = JSON.stringify(action);
updateRule.run(name, enabled ? 1 : 0, triggerType, triggerData, action.type, actionData, id);
const updated = getRuleById.get(id);
res.json(formatRule(updated));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/rules/:id - admin only
app.delete('/api/rules/:id', requireAuth, requireAdmin, (req, res) => {
try {
const { id } = req.params;
const existing = getRuleById.get(id);
if (!existing) {
return res.status(404).json({ error: 'Rule not found' });
}
deleteRule.run(id);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// API: Devices
app.get('/api/devices', (req, res) => {
try {