notXCS/routes/api/v1/ingame.js
2026-03-08 20:48:45 -06:00

174 lines
5.5 KiB
JavaScript

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);
});
});
});
});