Add download kit feature parity to xcs
This commit is contained in:
parent
ee8811184b
commit
5480ca0c84
|
|
@ -40,6 +40,7 @@
|
||||||
<div id="place-view" class="hidden">
|
<div id="place-view" class="hidden">
|
||||||
<div class="main-header">
|
<div class="main-header">
|
||||||
<h1 id="place-title"></h1>
|
<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>
|
<button class="danger" id="delete-place-btn">Delete place</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,47 @@ document.getElementById('delete-place-btn').addEventListener('click', async () =
|
||||||
await loadPlaces();
|
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', () => {
|
document.getElementById('toggle-api-key-btn').addEventListener('click', () => {
|
||||||
const input = document.getElementById('place-api-key-display');
|
const input = document.getElementById('place-api-key-display');
|
||||||
const btn = document.getElementById('toggle-api-key-btn');
|
const btn = document.getElementById('toggle-api-key-btn');
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const { requireAuth, ROLES } = require('../lib/auth');
|
const { requireAuth, ROLES } = require('../lib/auth');
|
||||||
const { generatePlaceId, generateApiKey, generateReaderId } = require('../lib/generators');
|
const { generatePlaceId, generateApiKey, generateReaderId } = require('../lib/generators');
|
||||||
|
|
||||||
|
const KIT_TEMPLATE_PATH = path.join(__dirname, '..', 'xcs-template.rbxmx');
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
|
|
||||||
module.exports = (dbInit) => {
|
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)
|
// Delete a place (and its readers/acl entries/access groups/scan logs/shared access grants)
|
||||||
router.delete('/places/:placeId', loadPlace, requireOwnerOrBypass, (req, res) => {
|
router.delete('/places/:placeId', loadPlace, requireOwnerOrBypass, (req, res) => {
|
||||||
db.all(`SELECT id FROM access_points WHERE placeId = ?`, [req.place.id], (err, accessPoints) => {
|
db.all(`SELECT id FROM access_points WHERE placeId = ?`, [req.place.id], (err, accessPoints) => {
|
||||||
|
|
|
||||||
1199
xcs-template.rbxmx
Normal file
1199
xcs-template.rbxmx
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue