improve folder config

This commit is contained in:
lelo 2025-12-17 14:05:40 +00:00
parent c797960fa3
commit d7bdb646fd
3 changed files with 187 additions and 57 deletions

17
app.py
View File

@ -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', 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/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.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/<secret>')
@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', 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/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)) app.add_url_rule('/admin/search_db_analyzer/folders', view_func=auth.require_admin(sdb.search_db_folders))

42
auth.py
View File

@ -117,22 +117,7 @@ def require_secret(f):
if not is_valid_token(token_in_session): if not is_valid_token(token_in_session):
session['valid_tokens'].remove(token_in_session) session['valid_tokens'].remove(token_in_session)
# 5) Build session['folders'] fresh from the valid secrets # Check for admin access first
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']
args_admin = request.args.get('admin', None) args_admin = request.args.get('admin', None)
config_admin = app_config.get('ADMIN_KEY', 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}") print(f"Admin access denied with key: {args_admin}")
session['admin'] = False 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 # 6) If we have folders, proceed; otherwise show index
if session['folders']: if session['folders']:
# assume since visitor has a valid secret, they are ok with annonymous tracking # assume since visitor has a valid secret, they are ok with annonymous tracking

View File

@ -39,6 +39,17 @@
let editing = new Set(); let editing = new Set();
let pendingDelete = null; 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 // helper to format DD.MM.YYYY → YYYY-MM-DD
function formatISO(d) { function formatISO(d) {
const [dd, mm, yyyy] = d.split('.'); const [dd, mm, yyyy] = d.split('.');
@ -87,40 +98,113 @@
wrapper.className = 'card mb-3'; wrapper.className = 'card mb-3';
wrapper.dataset.secret = key; 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 = `<strong>Ordner: ${folderNames}${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}</strong><i class="bi bi-chevron-down"></i>`;
cardHeader.appendChild(headerTitle);
wrapper.appendChild(cardHeader);
// Collapsible body
const collapseDiv = document.createElement('div');
collapseDiv.className = 'collapse';
collapseDiv.id = collapseId;
const body = document.createElement('div'); const body = document.createElement('div');
body.className = `card-body ${cls}`; body.className = `card-body ${cls}`;
// header // Create row for two-column layout (only if not editing)
const h5 = document.createElement('h5'); if (!isEdit) {
folderNames = rec.folders.map(f => f.foldername).join(', '); const topRow = document.createElement('div');
h5.innerHTML = `Ordner: ${folderNames}${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}`; topRow.className = 'row mb-3';
body.appendChild(h5);
// 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 // validity input
const secDiv = document.createElement('div'); const valDiv = document.createElement('div');
secDiv.className = 'mb-2'; valDiv.className = 'mb-2';
secDiv.innerHTML = `Secret: `; valDiv.innerHTML = `Gültig bis: `;
const secInput = document.createElement('input'); const valInput = document.createElement('input');
secInput.className = 'form-control'; valInput.className = 'form-control';
secInput.type = 'text'; valInput.type = 'date';
secInput.value = rec.secret; valInput.value = formatISO(rec.validity);
secInput.readOnly = !isEdit; valInput.readOnly = true;
secInput.dataset.field = 'secret'; valInput.dataset.field = 'validity';
secDiv.appendChild(secInput); valDiv.appendChild(valInput);
body.appendChild(secDiv); 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 // validity input
const valDiv = document.createElement('div'); const valDiv = document.createElement('div');
valDiv.className = 'mb-2'; valDiv.className = 'mb-2';
valDiv.innerHTML = `Gültig bis: `; valDiv.innerHTML = `Gültig bis: `;
const valInput = document.createElement('input'); const valInput = document.createElement('input');
valInput.className = 'form-control'; valInput.className = 'form-control';
valInput.type = 'date'; valInput.type = 'date';
valInput.value = formatISO(rec.validity); valInput.value = formatISO(rec.validity);
valInput.readOnly = !isEdit; valInput.readOnly = false;
valInput.dataset.field = 'validity'; valInput.dataset.field = 'validity';
valDiv.appendChild(valInput); valDiv.appendChild(valInput);
body.appendChild(valDiv); body.appendChild(valDiv);
}
// folders // folders
const folderHeader = document.createElement('h6'); const folderHeader = document.createElement('h6');
@ -219,22 +303,23 @@
if (isEdit) { if (isEdit) {
const saveBtn = document.createElement('button'); 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.type = 'button';
saveBtn.textContent = 'speichern'; saveBtn.textContent = 'speichern';
saveBtn.addEventListener('click', () => saveRec(key)); saveBtn.addEventListener('click', () => saveRec(key));
actions.appendChild(saveBtn); actions.appendChild(saveBtn);
} else {
const editBtn = document.createElement('button'); const cancelBtn = document.createElement('button');
editBtn.className = 'btn btn-warning btn-sm'; cancelBtn.className = 'btn btn-secondary btn-sm';
editBtn.type = 'button'; cancelBtn.type = 'button';
editBtn.textContent = 'bearbeiten'; cancelBtn.textContent = 'abbrechen';
editBtn.addEventListener('click', () => editRec(key)); cancelBtn.addEventListener('click', () => cancelEdit(key));
actions.appendChild(editBtn); actions.appendChild(cancelBtn);
} }
body.appendChild(actions); body.appendChild(actions);
wrapper.appendChild(body); collapseDiv.appendChild(body);
wrapper.appendChild(collapseDiv);
cont.appendChild(wrapper); cont.appendChild(wrapper);
}); });
} }
@ -249,9 +334,11 @@
} }
// CRUD helpers // CRUD helpers
function editRec(secret) { function cancelEdit(secret) {
editing.add(secret); editing.delete(secret);
render(); // 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) { function cloneRec(secret) {
const idx = data.findIndex(r => r.secret === secret); const idx = data.findIndex(r => r.secret === secret);
@ -268,6 +355,14 @@
data.splice(idx+1, 0, rec); data.splice(idx+1, 0, rec);
editing.add(rec.secret); editing.add(rec.secret);
render(); 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) { async function renewRec(secret) {
// find current record // find current record
@ -364,6 +459,14 @@
data.push({ secret: newSecret, validity: new Date().toISOString().slice(0,10), folders: [] }); data.push({ secret: newSecret, validity: new Date().toISOString().slice(0,10), folders: [] });
editing.add(newSecret); editing.add(newSecret);
render(); 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(); loadData();
}); });