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 @@
-
-
-
Readers
+
No readers yet for this place.
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