diff --git a/app.py b/app.py index bf2d4f3..57965c2 100755 --- a/app.py +++ b/app.py @@ -63,6 +63,23 @@ app.add_url_rule('/searchcommand', view_func=search.searchcommand, methods=['POS app.add_url_rule('/admin/folder_secret_config_editor', view_func=auth.require_admin(fsce.folder_secret_config_editor), methods=['GET', 'POST']) app.add_url_rule('/admin/folder_secret_config_editor/data', view_func=auth.require_admin(auth.load_folder_config)) app.add_url_rule('/admin/folder_secret_config_editor/action', view_func=auth.require_admin(fsce.folder_secret_config_action), methods=['POST']) + +@app.route('/admin/generate_qr/') +@auth.require_admin +def generate_qr_code(secret): + scheme = request.scheme + host = request.host + url = f"{scheme}://{host}?secret={secret}" + qr = qrcode.QRCode(version=1, box_size=10, border=4) + qr.add_data(url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buffer = io.BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + img_base64 = base64.b64encode(buffer.getvalue()).decode('ascii') + return jsonify({'qr_code': img_base64}) + app.add_url_rule('/admin/search_db_analyzer', view_func=auth.require_admin(sdb.search_db_analyzer)) app.add_url_rule('/admin/search_db_analyzer/query', view_func=auth.require_admin(sdb.search_db_query), methods=['POST']) app.add_url_rule('/admin/search_db_analyzer/folders', view_func=auth.require_admin(sdb.search_db_folders)) diff --git a/auth.py b/auth.py index 00a7854..ef17168 100644 --- a/auth.py +++ b/auth.py @@ -117,22 +117,7 @@ def require_secret(f): if not is_valid_token(token_in_session): session['valid_tokens'].remove(token_in_session) - # 5) Build session['folders'] fresh from the valid secrets - session['folders'] = {} - for secret_in_session in session.get('valid_secrets', []): - config_item = next( - (c for c in folder_config if c['secret'] == secret_in_session), - None - ) - if config_item: - for folder_info in config_item['folders']: - session['folders'][folder_info['foldername']] = folder_info['folderpath'] - - for token_in_session in session.get('valid_tokens', []): - token_item = decode_token(token_in_session) - for folder_info in token_item['folders']: - session['folders'][folder_info['foldername']] = folder_info['folderpath'] - + # Check for admin access first args_admin = request.args.get('admin', None) config_admin = app_config.get('ADMIN_KEY', None) @@ -144,6 +129,31 @@ def require_secret(f): print(f"Admin access denied with key: {args_admin}") session['admin'] = False + # 5) Build session['folders'] fresh from the valid secrets + # If admin, grant access to ALL folders + session['folders'] = {} + + if is_admin(): + # Admin gets access to all folders in the config + for config_item in folder_config: + for folder_info in config_item['folders']: + session['folders'][folder_info['foldername']] = folder_info['folderpath'] + else: + # Normal users only get folders from their valid secrets/tokens + for secret_in_session in session.get('valid_secrets', []): + config_item = next( + (c for c in folder_config if c['secret'] == secret_in_session), + None + ) + if config_item: + for folder_info in config_item['folders']: + session['folders'][folder_info['foldername']] = folder_info['folderpath'] + + for token_in_session in session.get('valid_tokens', []): + token_item = decode_token(token_in_session) + for folder_info in token_item['folders']: + session['folders'][folder_info['foldername']] = folder_info['folderpath'] + # 6) If we have folders, proceed; otherwise show index if session['folders']: # assume since visitor has a valid secret, they are ok with annonymous tracking diff --git a/templates/folder_secret_config_editor.html b/templates/folder_secret_config_editor.html index 2c11342..c4810f1 100644 --- a/templates/folder_secret_config_editor.html +++ b/templates/folder_secret_config_editor.html @@ -39,6 +39,17 @@ 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('.'); @@ -87,40 +98,113 @@ 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}`; - // header - const h5 = document.createElement('h5'); - folderNames = rec.folders.map(f => f.foldername).join(', '); - h5.innerHTML = `Ordner: ${folderNames}${expired ? ' ! abgelaufen !' : ''}`; - body.appendChild(h5); + // 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); - // 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 = !isEdit; - 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 = 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 = !isEdit; - valInput.dataset.field = 'validity'; - valDiv.appendChild(valInput); - body.appendChild(valDiv); + // 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'); @@ -219,22 +303,23 @@ if (isEdit) { const saveBtn = document.createElement('button'); - saveBtn.className = 'btn btn-success btn-sm'; + saveBtn.className = 'btn btn-success btn-sm me-2'; saveBtn.type = 'button'; saveBtn.textContent = 'speichern'; saveBtn.addEventListener('click', () => saveRec(key)); actions.appendChild(saveBtn); - } else { - const editBtn = document.createElement('button'); - editBtn.className = 'btn btn-warning btn-sm'; - editBtn.type = 'button'; - editBtn.textContent = 'bearbeiten'; - editBtn.addEventListener('click', () => editRec(key)); - actions.appendChild(editBtn); + + 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); - wrapper.appendChild(body); + collapseDiv.appendChild(body); + wrapper.appendChild(collapseDiv); cont.appendChild(wrapper); }); } @@ -249,9 +334,11 @@ } // CRUD helpers - function editRec(secret) { - editing.add(secret); - render(); + 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); @@ -268,6 +355,14 @@ 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 @@ -364,6 +459,14 @@ data.push({ secret: newSecret, validity: new Date().toISOString().slice(0,10), folders: [] }); editing.add(newSecret); render(); + // Auto-expand the new item + setTimeout(() => { + const collapseEl = document.getElementById(`collapse-${newSecret}`); + if (collapseEl) { + const bsCollapse = new bootstrap.Collapse(collapseEl, { toggle: false }); + bsCollapse.show(); + } + }, 100); }); loadData(); });