Add ACL management UI for readers (users, card numbers, groups, allow-all)
This commit is contained in:
parent
5961cba546
commit
67c2252f43
|
|
@ -304,3 +304,68 @@ button.danger {
|
|||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.acl-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.acl-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.acl-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.acl-entry .acl-description {
|
||||
flex: 1;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.badge.type-badge {
|
||||
background: rgba(79, 124, 255, 0.15);
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,51 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="acl-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2>Allowed persons & credentials — <span id="acl-reader-name"></span></h2>
|
||||
<button class="secondary" id="acl-close-btn">Close</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Add entry</h3>
|
||||
<form id="add-acl-form" class="inline-form">
|
||||
<div class="field-group">
|
||||
<label for="acl-type">Type</label>
|
||||
<select id="acl-type">
|
||||
<option value="0">User ID</option>
|
||||
<option value="1">Card number</option>
|
||||
<option value="2">Group exact rank</option>
|
||||
<option value="3">Group minimum rank</option>
|
||||
<option value="4">Allow all</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-group" id="acl-data-group">
|
||||
<label for="acl-data">User ID</label>
|
||||
<input type="text" id="acl-data">
|
||||
</div>
|
||||
<div class="field-group hidden" id="acl-group-id-group">
|
||||
<label for="acl-group-id">Group ID</label>
|
||||
<input type="text" id="acl-group-id">
|
||||
</div>
|
||||
<div class="field-group hidden" id="acl-group-rank-group">
|
||||
<label for="acl-group-rank">Rank</label>
|
||||
<input type="number" id="acl-group-rank" min="0">
|
||||
</div>
|
||||
<button type="submit" class="primary">Add entry</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Entries</h3>
|
||||
<div id="acl-empty" class="empty-state hidden">No entries yet. Access will be denied for everyone until an entry is added.</div>
|
||||
<ul class="acl-list" id="acl-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -89,12 +89,14 @@ function renderReaders(accessPoints) {
|
|||
</div>
|
||||
<div class="actions">
|
||||
<button class="secondary save-btn">Save</button>
|
||||
<button class="secondary acl-btn">Manage access</button>
|
||||
<button class="danger delete-btn">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.querySelector('.save-btn').addEventListener('click', () => saveReader(ap.id, card));
|
||||
card.querySelector('.delete-btn').addEventListener('click', () => deleteReader(ap.id));
|
||||
card.querySelector('.acl-btn').addEventListener('click', () => openAclModal(ap.id, ap.name));
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
|
@ -185,4 +187,160 @@ document.getElementById('logout-btn').addEventListener('click', async () => {
|
|||
window.location.href = '/login.html';
|
||||
});
|
||||
|
||||
// --- ACL (allowed persons / credentials) management ---
|
||||
|
||||
let currentAclReaderId = null;
|
||||
|
||||
const ACL_TYPE_LABELS = {
|
||||
0: 'User ID',
|
||||
1: 'Card number',
|
||||
2: 'Group exact rank',
|
||||
3: 'Group min rank',
|
||||
4: 'Allow all'
|
||||
};
|
||||
|
||||
function aclEntryDescription(entry) {
|
||||
switch (entry.type) {
|
||||
case 0:
|
||||
return `User ID: ${entry.data}`;
|
||||
case 1:
|
||||
return `Card number: ${entry.data}`;
|
||||
case 2: {
|
||||
const [group, rank] = entry.data.split(':');
|
||||
return `Group ${group}, exact rank ${rank}`;
|
||||
}
|
||||
case 3: {
|
||||
const [group, rank] = entry.data.split(':');
|
||||
return `Group ${group}, rank ${rank}+`;
|
||||
}
|
||||
case 4:
|
||||
return 'Everyone';
|
||||
default:
|
||||
return entry.data;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAclFormFields() {
|
||||
const type = parseInt(document.getElementById('acl-type').value, 10);
|
||||
const dataGroup = document.getElementById('acl-data-group');
|
||||
const groupIdGroup = document.getElementById('acl-group-id-group');
|
||||
const groupRankGroup = document.getElementById('acl-group-rank-group');
|
||||
const dataLabel = dataGroup.querySelector('label');
|
||||
const dataInput = document.getElementById('acl-data');
|
||||
|
||||
dataGroup.classList.add('hidden');
|
||||
groupIdGroup.classList.add('hidden');
|
||||
groupRankGroup.classList.add('hidden');
|
||||
dataInput.required = false;
|
||||
|
||||
if (type === 0) {
|
||||
dataLabel.textContent = 'User ID';
|
||||
dataGroup.classList.remove('hidden');
|
||||
dataInput.required = true;
|
||||
} else if (type === 1) {
|
||||
dataLabel.textContent = 'Card number';
|
||||
dataGroup.classList.remove('hidden');
|
||||
dataInput.required = true;
|
||||
} else if (type === 2 || type === 3) {
|
||||
groupIdGroup.classList.remove('hidden');
|
||||
groupRankGroup.classList.remove('hidden');
|
||||
}
|
||||
// type === 4 (Allow all) needs no extra input
|
||||
}
|
||||
|
||||
document.getElementById('acl-type').addEventListener('change', updateAclFormFields);
|
||||
updateAclFormFields();
|
||||
|
||||
async function openAclModal(readerId, readerName) {
|
||||
currentAclReaderId = readerId;
|
||||
document.getElementById('acl-reader-name').textContent = readerName;
|
||||
document.getElementById('acl-modal').classList.remove('hidden');
|
||||
document.getElementById('add-acl-form').reset();
|
||||
updateAclFormFields();
|
||||
await loadAcl();
|
||||
}
|
||||
|
||||
function closeAclModal() {
|
||||
currentAclReaderId = null;
|
||||
document.getElementById('acl-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function loadAcl() {
|
||||
if (!currentAclReaderId) return;
|
||||
const data = await api(`/dashboard/places/${encodeURIComponent(currentPlaceId)}/access-points/${encodeURIComponent(currentAclReaderId)}/acl`);
|
||||
renderAclList(data.acl || []);
|
||||
}
|
||||
|
||||
function renderAclList(entries) {
|
||||
const list = document.getElementById('acl-list');
|
||||
const empty = document.getElementById('acl-empty');
|
||||
list.innerHTML = '';
|
||||
|
||||
if (entries.length === 0) {
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'acl-entry';
|
||||
li.innerHTML = `
|
||||
<span class="badge type-badge">${escapeHtml(ACL_TYPE_LABELS[entry.type] || 'Unknown')}</span>
|
||||
<span class="acl-description">${escapeHtml(aclEntryDescription(entry))}</span>
|
||||
<button class="danger acl-delete-btn">Remove</button>
|
||||
`;
|
||||
li.querySelector('.acl-delete-btn').addEventListener('click', () => deleteAclEntry(entry.id));
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteAclEntry(aclId) {
|
||||
if (!currentAclReaderId) return;
|
||||
if (!confirm('Remove this entry?')) return;
|
||||
|
||||
await api(`/dashboard/places/${encodeURIComponent(currentPlaceId)}/access-points/${encodeURIComponent(currentAclReaderId)}/acl/${encodeURIComponent(aclId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
await loadAcl();
|
||||
}
|
||||
|
||||
document.getElementById('add-acl-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!currentAclReaderId) return;
|
||||
|
||||
const type = parseInt(document.getElementById('acl-type').value, 10);
|
||||
let data;
|
||||
|
||||
if (type === 0 || type === 1) {
|
||||
data = document.getElementById('acl-data').value.trim();
|
||||
if (!data) return;
|
||||
} else if (type === 2 || type === 3) {
|
||||
const groupId = document.getElementById('acl-group-id').value.trim();
|
||||
const rank = document.getElementById('acl-group-rank').value.trim();
|
||||
if (!groupId || rank === '') return;
|
||||
data = `${groupId}:${rank}`;
|
||||
} else if (type === 4) {
|
||||
data = '*';
|
||||
}
|
||||
|
||||
const result = await api(`/dashboard/places/${encodeURIComponent(currentPlaceId)}/access-points/${encodeURIComponent(currentAclReaderId)}/acl`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, data })
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('add-acl-form').reset();
|
||||
updateAclFormFields();
|
||||
await loadAcl();
|
||||
} else {
|
||||
alert(result.message || 'Failed to add entry');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('acl-close-btn').addEventListener('click', closeAclModal);
|
||||
document.getElementById('acl-modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'acl-modal') closeAclModal();
|
||||
});
|
||||
|
||||
init();
|
||||
|
|
|
|||
Loading…
Reference in a new issue