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/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/<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/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))

42
auth.py
View File

@ -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

View File

@ -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 = `<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');
body.className = `card-body ${cls}`;
// header
const h5 = document.createElement('h5');
folderNames = rec.folders.map(f => f.foldername).join(', ');
h5.innerHTML = `Ordner: ${folderNames}${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}`;
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();
});