diff --git a/public/css/style.css b/public/css/style.css index 9c6c743..19b030f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -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; +} diff --git a/public/dashboard.html b/public/dashboard.html index 4d0bb5a..1a9e1fe 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -61,6 +61,51 @@ + + + diff --git a/public/js/dashboard.js b/public/js/dashboard.js index f8f63a8..ca81190 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -89,12 +89,14 @@ function renderReaders(accessPoints) {
+
`; 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 = ` + ${escapeHtml(ACL_TYPE_LABELS[entry.type] || 'Unknown')} + ${escapeHtml(aclEntryDescription(entry))} + + `; + 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();