const express = require('express'); const router = express.Router(); let db; module.exports = (dbInit) => { db = dbInit; return router; }; function requireAuth(req, res, next) { if (!req.session || !req.session.userId) { return res.status(401).json({ success: false, message: "Not authenticated" }); } next(); } function loadOwnedPlace(req, res, next) { const { placeId } = req.params; db.get(`SELECT * FROM places WHERE id = ? AND ownerId = ?`, [placeId, req.session.userId], (err, place) => { if (err) { console.error('Failed to retrieve place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (!place) { return res.status(404).json({ success: false, message: "Place not found" }); } req.place = place; next(); }); } router.use(requireAuth); // List places owned by the current user router.get('/places', (req, res) => { db.all(`SELECT * FROM places WHERE ownerId = ?`, [req.session.userId], (err, places) => { if (err) { console.error('Failed to retrieve places:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, places }); }); }); // Create a new place router.post('/places', (req, res) => { const { id, apiKey, settings } = req.body || {}; if (!id || !apiKey) { return res.status(400).json({ success: false, message: "Place id and apiKey are required" }); } db.get(`SELECT id FROM places WHERE id = ?`, [id], (err, existing) => { if (err) { console.error('Failed to check for existing place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (existing) { return res.status(409).json({ success: false, message: "A place with that id already exists" }); } db.run(`INSERT INTO places (id, apiKey, settings, ownerId) VALUES (?, ?, ?, ?)`, [id, apiKey, settings || '{}', req.session.userId], (err) => { if (err) { console.error('Failed to create place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, place: { id, apiKey, settings: settings || '{}', ownerId: req.session.userId } }); }); }); }); // Get a single place with its access points (readers) router.get('/places/:placeId', loadOwnedPlace, (req, res) => { db.all(`SELECT * FROM access_points WHERE placeId = ?`, [req.place.id], (err, accessPoints) => { if (err) { console.error('Failed to retrieve access points:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, place: req.place, accessPoints }); }); }); // Update a place's apiKey/settings router.put('/places/:placeId', loadOwnedPlace, (req, res) => { const apiKey = req.body.apiKey !== undefined ? req.body.apiKey : req.place.apiKey; const settings = req.body.settings !== undefined ? req.body.settings : req.place.settings; db.run(`UPDATE places SET apiKey = ?, settings = ? WHERE id = ?`, [apiKey, settings, req.place.id], (err) => { if (err) { console.error('Failed to update place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); }); // Delete a place (and its readers/acl entries/access groups/scan logs) router.delete('/places/:placeId', loadOwnedPlace, (req, res) => { db.all(`SELECT id FROM access_points WHERE placeId = ?`, [req.place.id], (err, accessPoints) => { if (err) { console.error('Failed to retrieve access points:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } const apIds = accessPoints.map(ap => ap.id); const deleteAclAndAp = (cb) => { if (apIds.length === 0) return cb(); // Build a `?` placeholder per id so all values stay parameterized; apIds never contains raw user input here. const placeholders = apIds.map(() => '?').join(','); db.run(`DELETE FROM acl WHERE accessPoint IN (${placeholders})`, apIds, (err) => { if (err) return cb(err); db.run(`DELETE FROM scan_logs WHERE accessPoint IN (${placeholders})`, apIds, (err) => { if (err) return cb(err); db.run(`DELETE FROM access_points WHERE placeId = ?`, [req.place.id], cb); }); }); }; deleteAclAndAp((err) => { if (err) { console.error('Failed to delete readers for place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } db.all(`SELECT id FROM access_groups WHERE placeId = ?`, [req.place.id], (err, groups) => { if (err) { console.error('Failed to retrieve access groups for place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } const groupIds = groups.map(g => g.id); const deleteGroups = (cb) => { if (groupIds.length === 0) return cb(); const placeholders = groupIds.map(() => '?').join(','); db.run(`DELETE FROM access_group_members WHERE groupId IN (${placeholders})`, groupIds, (err) => { if (err) return cb(err); db.run(`DELETE FROM access_groups WHERE placeId = ?`, [req.place.id], cb); }); }; deleteGroups((err) => { if (err) { console.error('Failed to delete access groups for place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } db.run(`DELETE FROM places WHERE id = ?`, [req.place.id], (err) => { if (err) { console.error('Failed to delete place:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); }); }); }); }); }); function loadOwnedAccessPoint(req, res, next) { db.get(`SELECT * FROM access_points WHERE id = ? AND placeId = ?`, [req.params.accessPointId, req.place.id], (err, ap) => { if (err) { console.error('Failed to retrieve reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (!ap) { return res.status(404).json({ success: false, message: "Reader not found" }); } req.accessPoint = ap; next(); }); } // Create a new reader (access point) for a place router.post('/places/:placeId/access-points', loadOwnedPlace, (req, res) => { const { id, name } = req.body || {}; if (!id || !name) { return res.status(400).json({ success: false, message: "Reader id and name are required" }); } const enabled = req.body.enabled !== undefined ? (req.body.enabled ? 1 : 0) : 1; const unlockTime = req.body.unlockTime !== undefined ? req.body.unlockTime : 8; const armState = req.body.armState !== undefined ? req.body.armState : 1; db.get(`SELECT id FROM access_points WHERE id = ?`, [id], (err, existing) => { if (err) { console.error('Failed to check for existing reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (existing) { return res.status(409).json({ success: false, message: "A reader with that id already exists" }); } db.run( `INSERT INTO access_points (id, placeId, name, enabled, unlockTime, armState) VALUES (?, ?, ?, ?, ?, ?)`, [id, req.place.id, name, enabled, unlockTime, armState], (err) => { if (err) { console.error('Failed to create reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); } ); }); }); // Update a reader's settings router.put('/places/:placeId/access-points/:accessPointId', loadOwnedPlace, loadOwnedAccessPoint, (req, res) => { const ap = req.accessPoint; const name = req.body.name !== undefined ? req.body.name : ap.name; const enabled = req.body.enabled !== undefined ? (req.body.enabled ? 1 : 0) : ap.enabled; const unlockTime = req.body.unlockTime !== undefined ? req.body.unlockTime : ap.unlockTime; const armState = req.body.armState !== undefined ? req.body.armState : ap.armState; const readyData = req.body.readyData !== undefined ? req.body.readyData : ap.readyData; const disarmedData = req.body.disarmedData !== undefined ? req.body.disarmedData : ap.disarmedData; const grantedData = req.body.grantedData !== undefined ? req.body.grantedData : ap.grantedData; const deniedData = req.body.deniedData !== undefined ? req.body.deniedData : ap.deniedData; db.run( `UPDATE access_points SET name = ?, enabled = ?, unlockTime = ?, armState = ?, readyData = ?, disarmedData = ?, grantedData = ?, deniedData = ? WHERE id = ?`, [name, enabled, unlockTime, armState, readyData, disarmedData, grantedData, deniedData, ap.id], (err) => { if (err) { console.error('Failed to update reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); } ); }); // Delete a reader router.delete('/places/:placeId/access-points/:accessPointId', loadOwnedPlace, loadOwnedAccessPoint, (req, res) => { db.run(`DELETE FROM acl WHERE accessPoint = ?`, [req.accessPoint.id], (err) => { if (err) { console.error('Failed to delete ACL entries for reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } db.run(`DELETE FROM scan_logs WHERE accessPoint = ?`, [req.accessPoint.id], (err) => { if (err) { console.error('Failed to delete scan logs for reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } db.run(`DELETE FROM access_points WHERE id = ?`, [req.accessPoint.id], (err) => { if (err) { console.error('Failed to delete reader:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); }); }); }); // List scan logs for a reader (most recent first) router.get('/places/:placeId/access-points/:accessPointId/logs', loadOwnedPlace, loadOwnedAccessPoint, (req, res) => { const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500); db.all( `SELECT * FROM scan_logs WHERE accessPoint = ? ORDER BY scannedAt DESC, id DESC LIMIT ?`, [req.accessPoint.id, limit], (err, logs) => { if (err) { console.error('Failed to retrieve scan logs:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, logs }); } ); }); // List ACL entries for a reader router.get('/places/:placeId/access-points/:accessPointId/acl', loadOwnedPlace, loadOwnedAccessPoint, (req, res) => { db.all(`SELECT * FROM acl WHERE accessPoint = ?`, [req.accessPoint.id], (err, entries) => { if (err) { console.error('Failed to retrieve ACL entries:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, acl: entries }); }); }); // Add an ACL entry for a reader router.post('/places/:placeId/access-points/:accessPointId/acl', loadOwnedPlace, loadOwnedAccessPoint, (req, res) => { const { type, data } = req.body || {}; if (type === undefined || !data) { return res.status(400).json({ success: false, message: "ACL type and data are required" }); } db.run(`INSERT INTO acl (accessPoint, type, data) VALUES (?, ?, ?)`, [req.accessPoint.id, type, data], function (err) { if (err) { console.error('Failed to create ACL entry:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, id: this.lastID }); }); }); // Delete an ACL entry router.delete('/places/:placeId/access-points/:accessPointId/acl/:aclId', loadOwnedPlace, loadOwnedAccessPoint, (req, res) => { db.run(`DELETE FROM acl WHERE id = ? AND accessPoint = ?`, [req.params.aclId, req.accessPoint.id], (err) => { if (err) { console.error('Failed to delete ACL entry:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); }); function loadOwnedAccessGroup(req, res, next) { db.get(`SELECT * FROM access_groups WHERE id = ? AND placeId = ?`, [req.params.groupId, req.place.id], (err, group) => { if (err) { console.error('Failed to retrieve access group:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (!group) { return res.status(404).json({ success: false, message: "Access group not found" }); } req.accessGroup = group; next(); }); } // List access groups for a place router.get('/places/:placeId/access-groups', loadOwnedPlace, (req, res) => { db.all(`SELECT * FROM access_groups WHERE placeId = ?`, [req.place.id], (err, groups) => { if (err) { console.error('Failed to retrieve access groups:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, groups }); }); }); // Create an access group (e.g. "Staff") for a place router.post('/places/:placeId/access-groups', loadOwnedPlace, (req, res) => { const { name } = req.body || {}; if (!name) { return res.status(400).json({ success: false, message: "Access group name is required" }); } db.run(`INSERT INTO access_groups (placeId, name) VALUES (?, ?)`, [req.place.id, name], function (err) { if (err) { console.error('Failed to create access group:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, group: { id: this.lastID, placeId: req.place.id, name } }); }); }); // Rename an access group router.put('/places/:placeId/access-groups/:groupId', loadOwnedPlace, loadOwnedAccessGroup, (req, res) => { const name = req.body.name !== undefined ? req.body.name : req.accessGroup.name; if (!name) { return res.status(400).json({ success: false, message: "Access group name is required" }); } db.run(`UPDATE access_groups SET name = ? WHERE id = ?`, [name, req.accessGroup.id], (err) => { if (err) { console.error('Failed to update access group:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); }); // Delete an access group (and its members, and any ACL entries referencing it) router.delete('/places/:placeId/access-groups/:groupId', loadOwnedPlace, loadOwnedAccessGroup, (req, res) => { db.run(`DELETE FROM access_group_members WHERE groupId = ?`, [req.accessGroup.id], (err) => { if (err) { console.error('Failed to delete access group members:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } // Access group ACL entries store the group id in `data` with type 5. db.run(`DELETE FROM acl WHERE type = 5 AND data = ?`, [String(req.accessGroup.id)], (err) => { if (err) { console.error('Failed to delete ACL entries referencing access group:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } db.run(`DELETE FROM access_groups WHERE id = ?`, [req.accessGroup.id], (err) => { if (err) { console.error('Failed to delete access group:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); }); }); }); // List members (creds) of an access group router.get('/places/:placeId/access-groups/:groupId/members', loadOwnedPlace, loadOwnedAccessGroup, (req, res) => { db.all(`SELECT * FROM access_group_members WHERE groupId = ?`, [req.accessGroup.id], (err, members) => { if (err) { console.error('Failed to retrieve access group members:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, members }); }); }); // Add a member (user, card, group rank rule, etc) to an access group router.post('/places/:placeId/access-groups/:groupId/members', loadOwnedPlace, loadOwnedAccessGroup, (req, res) => { const { type, data } = req.body || {}; if (type === undefined || !data) { return res.status(400).json({ success: false, message: "Member type and data are required" }); } // Groups cannot contain other groups, to avoid nested/circular resolution. if (parseInt(type, 10) === 5) { return res.status(400).json({ success: false, message: "An access group cannot contain another access group" }); } db.run(`INSERT INTO access_group_members (groupId, type, data) VALUES (?, ?, ?)`, [req.accessGroup.id, type, data], function (err) { if (err) { console.error('Failed to add access group member:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true, id: this.lastID }); }); }); // Remove a member from an access group router.delete('/places/:placeId/access-groups/:groupId/members/:memberId', loadOwnedPlace, loadOwnedAccessGroup, (req, res) => { db.run(`DELETE FROM access_group_members WHERE id = ? AND groupId = ?`, [req.params.memberId, req.accessGroup.id], (err) => { if (err) { console.error('Failed to remove access group member:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } res.json({ success: true }); }); });