197 lines
7.7 KiB
HTML
197 lines
7.7 KiB
HTML
{# templates/mylinks.html #}
|
|
{% extends 'base.html' %}
|
|
|
|
{# page title #}
|
|
{% block title %}Edit Folder Config{% endblock %}
|
|
|
|
{# override navbar text: #}
|
|
{% block nav_brand %}Folder Config Editor{% endblock %}
|
|
|
|
{# page content #}
|
|
{% block content %}
|
|
<div class="container">
|
|
<h1>Edit Folder Config</h1>
|
|
<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 %}
|
|
<!-- jQuery and Bootstrap JS -->
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const ALPHABET = {{ alphabet|tojson }};
|
|
let data = [];
|
|
let editing = new Set();
|
|
let pendingDelete = null; // store secret pending confirmation
|
|
|
|
function loadData() {
|
|
$.getJSON('/admin/folder_secret_config_editor/data', res => { data = res; render(); });
|
|
}
|
|
|
|
function render() {
|
|
const $cont = $('#records').empty();
|
|
data.forEach(rec => {
|
|
const key = rec.secret;
|
|
const isEdit = editing.has(key);
|
|
const cls = isEdit ? 'unlocked' : 'locked';
|
|
|
|
// Determine if entry has expired
|
|
const validityISO = formatISO(rec.validity);
|
|
const expired = validityISO ? (new Date(validityISO) < new Date()) : false;
|
|
|
|
// Build card HTML
|
|
let html = `<div class="card mb-3 ${cls}" data-secret="${key}"><div class="card-body">`;
|
|
// Show exclamation if expired
|
|
html += `<h5>Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}</h5>`;
|
|
html += `<div class="mb-2">Secret: <input class="form-control" type="text" value="${rec.secret}" ${isEdit?'':'readonly'} data-field="secret"></div>`;
|
|
html += `<div class="mb-2">Validity: <input class="form-control" type="date" value="${validityISO}" ${isEdit?'':'readonly'} data-field="validity"></div>`;
|
|
html += `<h6>Ordner</h6>`;
|
|
rec.folders.forEach((f,i) => {
|
|
html += `<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px;">`;
|
|
html += `Ordnername: <div class="input-group mb-2">`;
|
|
html += `<input class="form-control" type="text" value="${f.foldername}" ${isEdit?'':'readonly'} data-field="foldername-${i}">`;
|
|
if(isEdit) html += `<button class="btn btn-outline-danger" onclick="removeFolder('${key}',${i})">Remove</button>`;
|
|
html += `</div>`;
|
|
html += `Ordnerpfad: <input class="form-control mb-2" type="text" value="${f.folderpath}" ${isEdit?'':'readonly'} data-field="folderpath-${i}">`;
|
|
html += `</div>`;
|
|
});
|
|
if(isEdit) html += `<button class="btn btn-sm btn-primary mb-2" onclick="addFolder('${key}')">Add Folder</button>`;
|
|
html += `<div>`;
|
|
if (!isEdit) html += `<a class="btn btn-secondary btn-sm me-2" href="/?secret=${rec.secret}">Link öffnen</a>`;
|
|
html += `<button class="btn btn-danger btn-sm me-2 delete-btn" data-secret="${key}">Delete</button>`;
|
|
html += `<button class="btn btn-secondary btn-sm me-2" onclick="cloneRec('${key}')">Clone</button>`;
|
|
if(isEdit) html += `<button class="btn btn-success btn-sm" onclick="saveRec('${key}')">Save</button>`;
|
|
else html += `<button class="btn btn-warning btn-sm" onclick="editRec('${key}')">Edit</button>`;
|
|
html += `</div></div></div>`;
|
|
$cont.append(html);
|
|
});
|
|
}
|
|
|
|
function formatISO(d) {
|
|
const p = d.split('.');
|
|
return p.length===3 ? `${p[2]}-${p[1]}-${p[0]}` : '';
|
|
}
|
|
|
|
// original delete function, called after confirmation
|
|
function deleteRec(secret) {
|
|
$.ajax({
|
|
url: '/admin/folder_secret_config_editor/action',
|
|
method: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({ action: 'delete', secret })
|
|
}).always(loadData);
|
|
}
|
|
|
|
// hook delete buttons to show modal
|
|
$(document).on('click', '.delete-btn', function() {
|
|
pendingDelete = $(this).data('secret');
|
|
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
|
|
modal.show();
|
|
});
|
|
|
|
// when user confirms deletion
|
|
$('.confirm-delete').on('click', function() {
|
|
if (pendingDelete) {
|
|
deleteRec(pendingDelete);
|
|
pendingDelete = null;
|
|
}
|
|
const modalEl = document.getElementById('confirmDeleteModal');
|
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
|
modal.hide();
|
|
});
|
|
|
|
function editRec(secret) { editing.add(secret); render(); }
|
|
function cloneRec(secret) {
|
|
const rec = data.find(r=>r.secret===secret);
|
|
const existing = data.map(r=>r.secret);
|
|
const clone = JSON.parse(JSON.stringify(rec));
|
|
clone.secret = generateSecret(existing);
|
|
data.splice(data.findIndex(r=>r.secret===secret)+1, 0, clone);
|
|
editing.add(clone.secret);
|
|
render();
|
|
}
|
|
function addFolder(secret) {
|
|
const rec = data.find(r=>r.secret===secret);
|
|
rec.folders.push({foldername:'',folderpath:''}); render();
|
|
}
|
|
function removeFolder(secret,i) {
|
|
const rec = data.find(r=>r.secret===secret);
|
|
rec.folders.splice(i,1); render();
|
|
}
|
|
function saveRec(secret) {
|
|
const rec = data.find(r=>r.secret===secret);
|
|
const $card = $(`[data-secret='${secret}']`);
|
|
const newSecret = $card.find('input[data-field=secret]').val();
|
|
const validity = $card.find('input[data-field=validity]').val();
|
|
const folders = rec.folders.map((_,i) => ({
|
|
foldername: $card.find(`[data-field=foldername-${i}]`).val(),
|
|
folderpath: $card.find(`[data-field=folderpath-${i}]`).val()
|
|
}));
|
|
if(!newSecret||data.some(r=>r.secret!==secret&&r.secret===newSecret)){
|
|
alert('Secret must be unique and non-empty');
|
|
return;
|
|
}
|
|
if(!validity){
|
|
alert('Validity required');
|
|
return;
|
|
}
|
|
if(folders.some(f=>!f.foldername||!f.folderpath)){
|
|
alert('Folder entries cannot be empty');
|
|
return;
|
|
}
|
|
$.ajax({
|
|
url:'/admin/folder_secret_config_editor/action',
|
|
method:'POST',
|
|
contentType:'application/json',
|
|
data: JSON.stringify({
|
|
action:'update',
|
|
oldSecret:secret,
|
|
newSecret,
|
|
validity,
|
|
folders
|
|
})
|
|
}).always(()=>{
|
|
editing.delete(secret);
|
|
loadData();
|
|
});
|
|
}
|
|
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;
|
|
}
|
|
|
|
$(document).ready(loadData);
|
|
$('#add-btn').click(() => {
|
|
const existing = data.map(r=>r.secret);
|
|
const newSecret = generateSecret(existing);
|
|
data.push({secret:newSecret, validity:new Date().toISOString().substr(0,10), folders:[]});
|
|
editing.add(newSecret);
|
|
render();
|
|
});
|
|
</script>
|
|
{% endblock %}
|