From e80564c61e9c34c06fbd0c318f1499b980b5a6f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:21:43 +0000 Subject: [PATCH] Server-generate reader IDs; condense add-reader UI into table header --- lib/generators.js | 16 +++++++++- public/css/style.css | 21 +++++++++++++ public/dashboard/index.html | 25 ++++++---------- public/js/dashboard.js | 6 ++-- routes/dashboard.js | 60 +++++++++++++++++++++---------------- 5 files changed, 82 insertions(+), 46 deletions(-) diff --git a/lib/generators.js b/lib/generators.js index b542bd7..d633df4 100644 --- a/lib/generators.js +++ b/lib/generators.js @@ -4,11 +4,25 @@ const crypto = require('crypto'); const API_KEY_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+'; const API_KEY_LENGTH = 64; +// Characters used when generating a random reader id: letters and digits only. +const READER_ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +const READER_ID_LENGTH = 16; + // Generates a new place id as a random UUID (v4). function generatePlaceId() { return crypto.randomUUID(); } +// Generates a random 16-character alphanumeric reader (access point) id. +// Uses crypto.randomInt for unbiased selection of each character. +function generateReaderId(length = READER_ID_LENGTH) { + let id = ''; + for (let i = 0; i < length; i++) { + id += READER_ID_CHARS[crypto.randomInt(READER_ID_CHARS.length)]; + } + return id; +} + // Generates a random alphanumeric+symbol API key of the given length (default 64 characters). // Uses crypto.randomInt for unbiased selection of each character (rather than bytes % length, // which would skew towards characters at the start of the alphabet). @@ -20,4 +34,4 @@ function generateApiKey(length = API_KEY_LENGTH) { return key; } -module.exports = { generatePlaceId, generateApiKey }; +module.exports = { generatePlaceId, generateApiKey, generateReaderId }; diff --git a/public/css/style.css b/public/css/style.css index 1837a7e..bb75912 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -224,6 +224,27 @@ button.danger { margin-top: 0; } +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.card-header h3 { + margin: 0; +} + +.card-header-form { + flex-wrap: nowrap; +} + +.card-header-form .field-group { + flex: none; + min-width: 180px; +} + .readers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); diff --git a/public/dashboard/index.html b/public/dashboard/index.html index 38a8656..642bd9f 100644 --- a/public/dashboard/index.html +++ b/public/dashboard/index.html @@ -79,22 +79,15 @@
-

Add reader

-
-
- - -
-
- - -
- -
-
- -
-

Readers

+
+

Readers

+
+
+ +
+ +
+
diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 7f26033..f6842e9 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -207,17 +207,15 @@ document.getElementById('add-reader-form').addEventListener('submit', async (e) e.preventDefault(); if (!currentPlaceId) return; - const id = document.getElementById('reader-id').value.trim(); const name = document.getElementById('reader-name').value.trim(); - if (!id || !name) return; + if (!name) return; const data = await api(`/dashboard/places/${encodeURIComponent(currentPlaceId)}/access-points`, { method: 'POST', - body: JSON.stringify({ id, name }) + body: JSON.stringify({ name }) }); if (data.success) { - document.getElementById('reader-id').value = ''; document.getElementById('reader-name').value = ''; selectPlace(currentPlaceId); } else { diff --git a/routes/dashboard.js b/routes/dashboard.js index 96cccef..988410d 100644 --- a/routes/dashboard.js +++ b/routes/dashboard.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); const { requireAuth, ROLES } = require('../lib/auth'); -const { generatePlaceId, generateApiKey } = require('../lib/generators'); +const { generatePlaceId, generateApiKey, generateReaderId } = require('../lib/generators'); let db; @@ -266,38 +266,48 @@ function loadOwnedAccessPoint(req, res, next) { }); } -// Create a new reader (access point) for a place +// Create a new reader (access point) for a place. The reader id is always +// generated server-side as a random 16-character alphanumeric string; no +// caller-supplied id is ever accepted. router.post('/places/:placeId/access-points', loadPlace, (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 { name } = req.body || {}; + if (!name) { + return res.status(400).json({ success: false, message: "Reader name is 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 }); + function tryInsert(attemptsLeft) { + const id = generateReaderId(); + 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) { + if (attemptsLeft <= 0) { + return res.status(500).json({ success: false, message: "Failed to generate a unique reader id" }); + } + return tryInsert(attemptsLeft - 1); + } + + 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, id }); + } + ); + }); + } + + tryInsert(5); }); // Update a reader's settings