let db; const express = require('express'); const router = express.Router(); module.exports = (dbInit) => { db = dbInit; return router; }; router.get('/', (req, res) => { res.json({ success: true, message: "Welcome to the NOTXCS API!" }); }); router.get('/ping', (req, res) => { // TODO: Implement a more robust health check. Probably will revolve around checking db conn and others. res.json({ success: true, message: "pong" }); }); router.get('/:placeId', (req, res) => { const placeId = req.params.placeId; const apiKey = req.query.apiKey; const universeid = req.query.universeid; // For some reason the official XCS api doesn't actually check the api key when getting settings? So we won't either. // XCS Also ignores universe ID. Probably used for some logging. We will ignore it. if (!placeId) { return res.status(404).json({ success: false, message: "What? How?" }); } db.get(`SELECT * FROM places WHERE id = ?`, [placeId], (err, row) => { if (err) { console.error('Failed to retrieve place settings:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (!row) { return res.status(404).json({ success: false, message: "Place not found" }); } db.all(`SELECT * FROM access_points WHERE placeId = ?`, [placeId], (err, accessPoints) => { if (err) { console.error('Failed to retrieve access points:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } let resp = { success: true, accessPoints: accessPoints.reduce((acc, ap) => { const armed = !!ap.armState; // Will have state 2 eventually for armed on schedule, but for now just 1 or 0. acc[ap.id] = { id: ap.id, name: ap.name, unlockTime: ap.unlockTime, config: { active: !!ap.enabled, armed, scanData: { ready: JSON.parse(ap.readyData || '{}'), disarmed: JSON.parse(ap.disarmedData || '{}') } } } return acc; }, {}) }; console.log(resp) res.json(resp); }); }); }); router.get('/:placeId/:accessPointId/onScan', async (req, res) => { // This one DOES check api key. const placeId = req.params.placeId; const accessPointId = req.params.accessPointId; const apiKey = req.query.apiKey; const userId = req.query.userId; const cardNumbers = req.query.cardNumbers ? req.query.cardNumbers.split(',') : []; const universeid = req.query.universeid || '0'; if (!placeId || !accessPointId) { return res.status(404).json({ success: false, message: "What? How?" }); } if (!apiKey) { return res.status(400).json({ success: false, message: "API key is required" }); } db.get(`SELECT * FROM places WHERE id = ? AND apiKey = ?`, [placeId, apiKey], (err, place) => { if (err) { console.error('Failed to retrieve place settings:', 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 or invalid API key" }); } db.get(`SELECT * FROM access_points WHERE id = ? AND placeId = ?`, [accessPointId, placeId], (err, ap) => { if (err) { console.error('Failed to retrieve access point:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } if (!ap) { return res.status(404).json({ success: false, message: "Access point not found" }); } db.all(`SELECT * FROM acl WHERE accessPoint = ?`, [accessPointId], async (err, aclEntries) => { if (err) { console.error('Failed to retrieve ACL entries:', err.message); return res.status(500).json({ success: false, message: "Internal server error" }); } let userGroups = await fetch(`https://groups.roblox.com/v1/users/${userId}/groups/roles`).then(r => r.json()).then(data => data.data || []).catch(() => []); userGroups = userGroups.map(g => ({group: g.group.id, rank: g.role.rank})); console.log('User groups:', userGroups); let grant_type; let granted = false; // Types: 0 = User ID; 1 = Card Number; 2 = Group Exact Rank; 3 = Group Min Rank, 4 = Allow All // Check user ID, then group ranks, then card numbers. for (const entry of aclEntries) { switch (entry.type) { case 0: // User ID if (entry.data == userId) { granted = true; } break; case 1: // Card Number if (cardNumbers.includes(entry.data)) { granted = true; } break; case 2: { // Group Exact Rank const [group, rank] = entry.data.split(':'); if (userGroups.some(g => g.group == group && g.rank == rank)) { granted = true; } break; } case 3: { // Group Min Rank const [group, rank] = entry.data.split(':'); if (userGroups.some(g => g.group == group && g.rank >= rank)) { granted = true; } break; } case 4: // Allow All granted = true; break; } if (granted) break; } const responseData = granted ? { success: true, grant_type: 'user_scan', response_code: 'access_granted', response_time: 0, // This is just analytics for how long their backend took to generate the response scan_data: JSON.parse(ap.grantedData || '{}') } : { success: true, grant_type: 'user_scan', response_code: 'access_denied', response_time: 0, scan_data: JSON.parse(ap.deniedData || '{}') } res.json(responseData); }); }); }); });