(() => { let ALPHABET = []; let data = []; let editing = new Set(); let pendingDelete = null; // QR Code generation function using backend async function generateQRCode(secret, imgElement) { try { const response = await fetch(`/admin/generate_qr/${encodeURIComponent(secret)}`); const data = await response.json(); imgElement.src = `data:image/png;base64,${data.qr_code}`; } catch (error) { console.error('Error generating QR code:', error); } } // helper to format DD.MM.YYYY → YYYY-MM-DD function formatISO(d) { const [dd, mm, yyyy] = d.split('.'); return (dd && mm && yyyy) ? `${yyyy}-${mm}-${dd}` : ''; } // load from server async function loadData() { try { const res = await fetch('/admin/folder_secret_config_editor/data'); data = await res.json(); render(); } catch (err) { console.error(err); } } // send delete/update actions async function sendAction(payload) { await fetch('/admin/folder_secret_config_editor/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); await loadData(); } // delete record after confirmation function deleteRec(secret) { sendAction({ action: 'delete', secret }); } // build all cards function render() { const cont = document.getElementById('records'); cont.innerHTML = ''; data.forEach(rec => { const key = rec.secret; const isEdit = editing.has(key); const expired = (formatISO(rec.validity) && new Date(formatISO(rec.validity)) < new Date()); const cls = isEdit ? 'unlocked' : 'locked'; // outer card const wrapper = document.createElement('div'); wrapper.className = 'card mb-3'; wrapper.dataset.secret = key; // Card header for collapse toggle const cardHeader = document.createElement('div'); cardHeader.className = 'card-header'; cardHeader.style.cursor = 'pointer'; const collapseId = `collapse-${key}`; cardHeader.setAttribute('data-bs-toggle', 'collapse'); cardHeader.setAttribute('data-bs-target', `#${collapseId}`); cardHeader.setAttribute('aria-expanded', 'false'); cardHeader.setAttribute('aria-controls', collapseId); const headerTitle = document.createElement('div'); headerTitle.className = 'd-flex justify-content-between align-items-center'; const folderNames = rec.folders.map(f => f.foldername).join(', '); headerTitle.innerHTML = `Ordner: ${folderNames}${expired ? ' ! abgelaufen !' : ''}`; cardHeader.appendChild(headerTitle); wrapper.appendChild(cardHeader); // Collapsible body const collapseDiv = document.createElement('div'); collapseDiv.className = 'collapse'; collapseDiv.id = collapseId; const body = document.createElement('div'); body.className = `card-body ${cls}`; // Create row for two-column layout (only if not editing) if (!isEdit) { const topRow = document.createElement('div'); topRow.className = 'row mb-3'; // Left column for secret and validity const leftCol = document.createElement('div'); leftCol.className = 'col-md-8'; // secret input const secDiv = document.createElement('div'); secDiv.className = 'mb-2'; secDiv.innerHTML = `Secret: `; const secInput = document.createElement('input'); secInput.className = 'form-control'; secInput.type = 'text'; secInput.value = rec.secret; secInput.readOnly = true; secInput.dataset.field = 'secret'; secDiv.appendChild(secInput); leftCol.appendChild(secDiv); // validity input const valDiv = document.createElement('div'); valDiv.className = 'mb-2'; valDiv.innerHTML = `Gültig bis: `; const valInput = document.createElement('input'); valInput.className = 'form-control'; valInput.type = 'date'; valInput.value = formatISO(rec.validity); valInput.readOnly = true; valInput.dataset.field = 'validity'; valDiv.appendChild(valInput); leftCol.appendChild(valDiv); topRow.appendChild(leftCol); // Right column for QR code const rightCol = document.createElement('div'); rightCol.className = 'col-md-4 text-center'; const qrImg = document.createElement('img'); qrImg.className = 'qr-code'; qrImg.style.maxWidth = '200px'; qrImg.style.width = '100%'; qrImg.style.border = '1px solid #ddd'; qrImg.style.padding = '10px'; qrImg.style.borderRadius = '5px'; qrImg.alt = 'QR Code'; generateQRCode(key, qrImg); rightCol.appendChild(qrImg); topRow.appendChild(rightCol); body.appendChild(topRow); } else { // When editing, show fields vertically without QR code // secret input const secDiv = document.createElement('div'); secDiv.className = 'mb-2'; secDiv.innerHTML = `Secret: `; const secInput = document.createElement('input'); secInput.className = 'form-control'; secInput.type = 'text'; secInput.value = rec.secret; secInput.readOnly = false; secInput.dataset.field = 'secret'; secDiv.appendChild(secInput); body.appendChild(secDiv); // validity input const valDiv = document.createElement('div'); valDiv.className = 'mb-2'; valDiv.innerHTML = `Gültig bis: `; const valInput = document.createElement('input'); valInput.className = 'form-control'; valInput.type = 'date'; valInput.value = formatISO(rec.validity); valInput.readOnly = false; valInput.dataset.field = 'validity'; valDiv.appendChild(valInput); body.appendChild(valDiv); } // folders const folderHeader = document.createElement('h6'); folderHeader.textContent = 'Ordner'; body.appendChild(folderHeader); rec.folders.forEach((f, i) => { const fwrap = document.createElement('div'); fwrap.style = 'border:1px solid #ccc; padding:10px; margin-bottom:10px; border-radius:5px;'; // name fwrap.appendChild(document.createTextNode("Ordnername: ")); const nameGroup = document.createElement('div'); nameGroup.className = 'input-group mb-2'; const nameInput = document.createElement('input'); nameInput.className = 'form-control'; nameInput.type = 'text'; nameInput.value = f.foldername; nameInput.readOnly = !isEdit; nameInput.dataset.field = `foldername-${i}`; nameGroup.appendChild(nameInput); if (isEdit) { const remBtn = document.createElement('button'); remBtn.className = 'btn btn-outline-danger'; remBtn.type = 'button'; remBtn.textContent = 'entfernen'; remBtn.addEventListener('click', () => removeFolder(key, i)); nameGroup.appendChild(remBtn); } fwrap.appendChild(nameGroup); // path fwrap.appendChild(document.createTextNode("Ordnerpfad: ")); const pathInput = document.createElement('input'); pathInput.className = 'form-control mb-2'; pathInput.type = 'text'; pathInput.value = f.folderpath; pathInput.readOnly = !isEdit; pathInput.dataset.field = `folderpath-${i}`; fwrap.appendChild(pathInput); body.appendChild(fwrap); }); if (isEdit) { const addFld = document.createElement('button'); addFld.className = 'btn btn-sm btn-primary mb-2'; addFld.type = 'button'; addFld.textContent = 'Ordner hinzufügen'; addFld.addEventListener('click', () => addFolder(key)); body.appendChild(addFld); } // actions row const actions = document.createElement('div'); if (!isEdit) { const openButton = document.createElement('button'); openButton.className = 'btn btn-secondary btn-sm me-2'; openButton.onclick = () => window.open(`/?secret=${rec.secret}`, '_self'); openButton.textContent = 'Link öffnen'; actions.appendChild(openButton); } if (!isEdit) { const openButton = document.createElement('button'); openButton.className = 'btn btn-secondary btn-sm me-2'; openButton.onclick = () => toClipboard(`${window.location.origin}/?secret=${rec.secret}`); openButton.textContent = 'Link kopieren'; actions.appendChild(openButton); } const delBtn = document.createElement('button'); delBtn.className = 'btn btn-danger btn-sm me-2 delete-btn'; delBtn.type = 'button'; delBtn.textContent = 'löschen'; delBtn.dataset.secret = key; actions.appendChild(delBtn); const cloneBtn = document.createElement('button'); cloneBtn.className = 'btn btn-secondary btn-sm me-2'; cloneBtn.type = 'button'; cloneBtn.textContent = 'clonen'; cloneBtn.addEventListener('click', () => cloneRec(key)); actions.appendChild(cloneBtn); if (isEdit || expired) { const renewBtn = document.createElement('button'); renewBtn.className = 'btn btn-info btn-sm me-2'; renewBtn.type = 'button'; renewBtn.textContent = 'erneuern'; renewBtn.addEventListener('click', () => renewRec(key)); actions.appendChild(renewBtn); } if (isEdit) { const saveBtn = document.createElement('button'); saveBtn.className = 'btn btn-success btn-sm me-2'; saveBtn.type = 'button'; saveBtn.textContent = 'speichern'; saveBtn.addEventListener('click', () => saveRec(key)); actions.appendChild(saveBtn); const cancelBtn = document.createElement('button'); cancelBtn.className = 'btn btn-secondary btn-sm'; cancelBtn.type = 'button'; cancelBtn.textContent = 'abbrechen'; cancelBtn.addEventListener('click', () => cancelEdit(key)); actions.appendChild(cancelBtn); } body.appendChild(actions); collapseDiv.appendChild(body); wrapper.appendChild(collapseDiv); cont.appendChild(wrapper); }); } // generate a unique secret function generateSecret(existing) { let s; do { s = Array.from({length:32}, () => ALPHABET[Math.floor(Math.random()*ALPHABET.length)]).join(''); } while (existing.includes(s)); return s; } // CRUD helpers function cancelEdit(secret) { editing.delete(secret); // If this was a new record that was never saved, remove it const originalExists = data.some(r => r.secret === secret && !editing.has(secret)); loadData(); // Reload to discard changes } function cloneRec(secret) { const idx = data.findIndex(r => r.secret === secret); const rec = JSON.parse(JSON.stringify(data[idx])); const existing = data.map(r => r.secret); rec.secret = generateSecret(existing); const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 35); // Format as DD.MM.YYYY for validity input const dd = String(futureDate.getDate()).padStart(2, '0'); const mm = String(futureDate.getMonth() + 1).padStart(2, '0'); const yyyy = futureDate.getFullYear(); rec.validity = `${dd}.${mm}.${yyyy}`; data.splice(idx+1, 0, rec); editing.add(rec.secret); render(); // Auto-expand the cloned item setTimeout(() => { const collapseEl = document.getElementById(`collapse-${rec.secret}`); if (collapseEl) { const bsCollapse = new bootstrap.Collapse(collapseEl, { toggle: false }); bsCollapse.show(); } }, 100); } async function renewRec(secret) { // find current record const rec = data.find(r => r.secret === secret); if (!rec) return; // generate a fresh unique secret const existing = data.map(r => r.secret); const newSecret = generateSecret(existing); // validity = today + 35 days, formatted as YYYY-MM-DD const future = new Date(); future.setDate(future.getDate() + 35); const yyyy = future.getFullYear(); const mm = String(future.getMonth() + 1).padStart(2, '0'); const dd = String(future.getDate()).padStart(2, '0'); const validity = `${yyyy}-${mm}-${dd}`; // keep folders unchanged const folders = rec.folders.map(f => ({ foldername: f.foldername, folderpath: f.folderpath })); // persist via existing endpoint await sendAction({ action: 'update', oldSecret: secret, newSecret, validity, folders }); } function addFolder(secret) { data.find(r => r.secret === secret).folders.push({foldername:'', folderpath:''}); render(); } function removeFolder(secret, i) { data.find(r => r.secret === secret).folders.splice(i,1); render(); } async function saveRec(secret) { const card = document.querySelector(`[data-secret="${secret}"]`); const newSecret = card.querySelector('input[data-field="secret"]').value.trim(); const validity = card.querySelector('input[data-field="validity"]').value; const rec = data.find(r => r.secret === secret); const folders = rec.folders.map((_, i) => ({ foldername: card.querySelector(`input[data-field="foldername-${i}"]`).value.trim(), folderpath: card.querySelector(`input[data-field="folderpath-${i}"]`).value.trim() })); if (!newSecret || data.some(r => r.secret !== secret && r.secret === newSecret)) { return alert('Secret must be unique and non-empty'); } if (!validity) { return alert('Validity required'); } if (folders.some(f => !f.foldername || !f.folderpath)) { return alert('Folder entries cannot be empty'); } await sendAction({ action: 'update', oldSecret: secret, newSecret, validity, folders }); editing.delete(secret); await loadData(); } let addBtnHandler = null; let deleteClickHandler = null; let confirmDeleteHandler = null; function bindHandlers() { const root = document.getElementById('records'); if (!root || root.dataset.bound) return false; root.dataset.bound = '1'; const dataScript = document.getElementById('folder-config-page-data'); if (dataScript) { try { const parsed = JSON.parse(dataScript.textContent || '{}'); if (Array.isArray(parsed.alphabet)) { ALPHABET = parsed.alphabet; } else if (typeof parsed.alphabet === 'string') { ALPHABET = parsed.alphabet.split(''); } else { ALPHABET = []; } } catch (err) { console.error('Failed to parse folder config data', err); } } addBtnHandler = () => { const existing = data.map(r => r.secret); const newSecret = generateSecret(existing); data.push({ secret: newSecret, validity: new Date().toISOString().slice(0,10), folders: [] }); editing.add(newSecret); render(); setTimeout(() => { const collapseEl = document.getElementById(`collapse-${newSecret}`); if (collapseEl) { const bsCollapse = new bootstrap.Collapse(collapseEl, { toggle: false }); bsCollapse.show(); } }, 100); }; const addBtn = document.getElementById('add-btn'); if (addBtn) addBtn.addEventListener('click', addBtnHandler); deleteClickHandler = (e) => { if (e.target.matches('.delete-btn')) { pendingDelete = e.target.dataset.secret; const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal')); modal.show(); } }; document.addEventListener('click', deleteClickHandler); confirmDeleteHandler = () => { if (pendingDelete) deleteRec(pendingDelete); pendingDelete = null; const modal = bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal')); if (modal) modal.hide(); }; const confirmBtn = document.querySelector('.confirm-delete'); if (confirmBtn) confirmBtn.addEventListener('click', confirmDeleteHandler); return true; } function unbindHandlers() { const addBtn = document.getElementById('add-btn'); if (addBtn && addBtnHandler) addBtn.removeEventListener('click', addBtnHandler); addBtnHandler = null; if (deleteClickHandler) document.removeEventListener('click', deleteClickHandler); deleteClickHandler = null; const confirmBtn = document.querySelector('.confirm-delete'); if (confirmBtn && confirmDeleteHandler) confirmBtn.removeEventListener('click', confirmDeleteHandler); confirmDeleteHandler = null; } function initFolderConfigPage() { data = []; editing = new Set(); pendingDelete = null; if (!bindHandlers()) return; loadData(); } function destroyFolderConfigPage() { unbindHandlers(); } if (window.PageRegistry && typeof window.PageRegistry.register === 'function') { window.PageRegistry.register('folder_secret_config', { init: initFolderConfigPage, destroy: destroyFolderConfigPage }); } })();