bethaus-app/templates/folder_secret_config_editor.html
2025-12-17 14:05:40 +00:00

476 lines
17 KiB
HTML

{# templates/mylinks.html #}
{% extends 'base.html' %}
{# page title #}
{% block title %}Ordnerkonfiguration{% endblock %}
{# page content #}
{% block content %}
<div class="container">
<h2>Ordnerkonfiguration</h2>
<div id="records"></div>
<button id="add-btn" class="btn btn-primary mt-3">Add New Record</button>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-labelledby="confirmDeleteLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmDeleteLabel">Confirm Deletion</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Do you really want to delete this record?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger confirm-delete">Delete</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const ALPHABET = {{ alphabet|tojson }};
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 = `<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}`;
// 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();
}
// modal handling for delete
document.addEventListener('click', e => {
if (e.target.matches('.delete-btn')) {
pendingDelete = e.target.dataset.secret;
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
modal.show();
}
});
document.querySelector('.confirm-delete').addEventListener('click', () => {
if (pendingDelete) deleteRec(pendingDelete);
pendingDelete = null;
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal'));
modal.hide();
});
// init
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-btn').addEventListener('click', () => {
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();
// 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();
});
</script>
{% endblock %}