Add download kit feature parity to xcs

This commit is contained in:
Christopher Cookman 2026-07-02 10:56:26 -06:00
parent ee8811184b
commit 5480ca0c84
4 changed files with 1265 additions and 0 deletions

View file

@ -40,6 +40,7 @@
<div id="place-view" class="hidden">
<div class="main-header">
<h1 id="place-title"></h1>
<button class="secondary" id="download-kit-btn">Download kit</button>
<button class="danger" id="delete-place-btn">Delete place</button>
</div>

View file

@ -234,6 +234,47 @@ document.getElementById('delete-place-btn').addEventListener('click', async () =
await loadPlaces();
});
document.getElementById('download-kit-btn').addEventListener('click', async () => {
if (!currentPlaceId) return;
const btn = document.getElementById('download-kit-btn');
const originalLabel = btn.textContent;
btn.textContent = 'Downloading...';
btn.disabled = true;
try {
const res = await fetch(`/dashboard/places/${encodeURIComponent(currentPlaceId)}/kit`);
if (res.status === 401) {
window.location.href = '/login';
return;
}
if (!res.ok) {
const data = await res.json().catch(() => ({}));
alert(data.message || 'Failed to download kit');
return;
}
const blob = await res.blob();
const disposition = res.headers.get('Content-Disposition') || '';
const match = disposition.match(/filename="([^"]+)"/);
const filename = match ? match[1] : `${currentPlaceId}.rbxmx`;
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
} catch (err) {
alert('Failed to download kit');
} finally {
btn.textContent = originalLabel;
btn.disabled = false;
}
});
document.getElementById('toggle-api-key-btn').addEventListener('click', () => {
const input = document.getElementById('place-api-key-display');
const btn = document.getElementById('toggle-api-key-btn');

View file

@ -1,8 +1,12 @@
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { requireAuth, ROLES } = require('../lib/auth');
const { generatePlaceId, generateApiKey, generateReaderId } = require('../lib/generators');
const KIT_TEMPLATE_PATH = path.join(__dirname, '..', 'xcs-template.rbxmx');
let db;
module.exports = (dbInit) => {
@ -182,6 +186,26 @@ router.post('/places/:placeId/regenerate-key', loadPlace, (req, res) => {
});
});
// Download the place's Roblox kit (xcs-template.rbxmx) with the placeId/apiKey placeholders
// filled in for this specific place. Available to anyone with access to the place.
router.get('/places/:placeId/kit', loadPlace, (req, res) => {
fs.readFile(KIT_TEMPLATE_PATH, 'utf8', (err, template) => {
if (err) {
console.error('Failed to read kit template:', err.message);
return res.status(500).json({ success: false, message: "Internal server error" });
}
const kit = template
.split('%%PLACEID%%').join(req.place.id)
.split('%%APIKEY%%').join(req.place.apiKey);
const filename = `${(req.place.name || req.place.id).replace(/[^a-zA-Z0-9-_]/g, '_')}.rbxmx`;
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(kit);
});
});
// Delete a place (and its readers/acl entries/access groups/scan logs/shared access grants)
router.delete('/places/:placeId', loadPlace, requireOwnerOrBypass, (req, res) => {
db.all(`SELECT id FROM access_points WHERE placeId = ?`, [req.place.id], (err, accessPoints) => {

1199
xcs-template.rbxmx Normal file

File diff suppressed because it is too large Load diff