Compare commits

..

3 Commits

Author SHA1 Message Date
9cbb58417e remove jquery 2025-05-05 17:13:33 +00:00
f7137e2d4c fix bootstrap implementation 2025-05-04 18:30:34 +00:00
fceeabe652 central config 2025-05-04 17:24:04 +00:00
9 changed files with 334 additions and 212 deletions

14
app.py
View File

@ -9,7 +9,6 @@ import mimetypes
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
import diskcache import diskcache
import threading import threading
import json
import time import time
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
import geoip2.database import geoip2.database
@ -24,11 +23,7 @@ import auth
import analytics as a import analytics as a
import folder_secret_config_editor as fsce import folder_secret_config_editor as fsce
with open("app_config.json", 'r') as file: app_config = auth.return_app_config()
app_config = json.load(file)
with open('folder_secret_config.json') as file:
folder_config = json.load(file)
cache_audio = diskcache.Cache('./filecache_audio', size_limit= app_config['filecache_size_limit_audio'] * 1024**3) cache_audio = diskcache.Cache('./filecache_audio', size_limit= app_config['filecache_size_limit_audio'] * 1024**3)
cache_image = diskcache.Cache('./filecache_image', size_limit= app_config['filecache_size_limit_image'] * 1024**3) cache_image = diskcache.Cache('./filecache_image', size_limit= app_config['filecache_size_limit_image'] * 1024**3)
@ -55,7 +50,7 @@ app.add_url_rule('/searchcommand', view_func=search.searchcommand, methods=['POS
app.add_url_rule('/songs_dashboard', view_func=a.songs_dashboard) app.add_url_rule('/songs_dashboard', view_func=a.songs_dashboard)
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(fsce.folder_secret_config_data)) 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'])
# Grab the HOST_RULE environment variable # Grab the HOST_RULE environment variable
@ -104,6 +99,7 @@ def list_directory_contents(directory, subpath):
""" """
directories = [] directories = []
files = [] files = []
folder_config = auth.return_folder_config()
transcription_dir = os.path.join(directory, "Transkription") transcription_dir = os.path.join(directory, "Transkription")
transcription_exists = os.path.isdir(transcription_dir) transcription_exists = os.path.isdir(transcription_dir)
@ -438,9 +434,10 @@ def get_transcript(subpath):
def create_share(subpath): def create_share(subpath):
scheme = request.scheme # current scheme (http or https) scheme = request.scheme # current scheme (http or https)
host = request.host host = request.host
if 'admin ' not in session and not session.get('admin'): if 'admin' not in session and not session.get('admin'):
return "Unauthorized", 403 return "Unauthorized", 403
folder_config = auth.return_folder_config()
paths = {} paths = {}
for item in folder_config: for item in folder_config:
for folder in item['folders']: for folder in item['folders']:
@ -550,6 +547,7 @@ def handle_request_initial_data():
@app.route('/<path:path>') @app.route('/<path:path>')
@auth.require_secret @auth.require_secret
def index(path): def index(path):
app_config = auth.return_app_config()
title_short = app_config.get('TITLE_SHORT', 'Default Title') title_short = app_config.get('TITLE_SHORT', 'Default Title')
title_long = app_config.get('TITLE_LONG' , 'Default Title') title_long = app_config.get('TITLE_LONG' , 'Default Title')

55
auth.py
View File

@ -12,29 +12,42 @@ import zlib
import hmac import hmac
import hashlib import hashlib
FOLDER_CONFIG_FILENAME = 'folder_secret_config.json'
APP_CONFIG_FILENAME = 'app_config.json'
# initial read of the config files
with open("app_config.json", 'r') as file: with open(APP_CONFIG_FILENAME, 'r') as file:
app_config = json.load(file) app_config = json.load(file)
with open('folder_secret_config.json') as file: with open(FOLDER_CONFIG_FILENAME) as file:
folder_config = json.load(file) folder_config = json.load(file)
# functions to be used by other modules
def load_folder_config():
global folder_config
with open(FOLDER_CONFIG_FILENAME) as file:
folder_config = json.load(file)
return folder_config
def load_app_config():
global app_config
with open(APP_CONFIG_FILENAME, 'r') as file:
app_config = json.load(file)
return app_config
def return_folder_config():
return folder_config
def return_app_config():
return app_config
def is_admin(): def is_admin():
""" """
Check if the user is an admin based on the session. Check if the user is an admin based on the session.
""" """
return session.get('admin', False) return session.get('admin', False)
def require_admin(f):
@wraps(f)
@require_secret
def decorated_function(*args, **kwargs):
if is_admin():
return f(*args, **kwargs)
else:
return "You don't have admin permission", 403
return decorated_function
def require_secret(f): def require_secret(f):
@wraps(f) @wraps(f)
@ -150,6 +163,24 @@ def require_secret(f):
) )
return decorated_function return decorated_function
def require_admin(f):
@wraps(f)
@require_secret
def decorated_function(*args, **kwargs):
if is_admin():
return f(*args, **kwargs)
else:
return "You don't have admin permission", 403
return decorated_function
@require_admin
def save_folder_config(data):
global folder_config
folder_config = data
with open(FOLDER_CONFIG_FILENAME, 'w') as file:
json.dump(folder_config, file, indent=4)
return folder_config
@require_secret @require_secret
def mylinks(): def mylinks():
scheme = request.scheme # current scheme (http or https) scheme = request.scheme # current scheme (http or https)

View File

@ -7,39 +7,18 @@ import string
import auth import auth
DATA_FILE = 'folder_secret_config.json'
# Secret alphabet # Secret alphabet
ALPHABET = string.ascii_letters + string.digits ALPHABET = string.ascii_letters + string.digits
@auth.require_admin
def load_data():
with open(DATA_FILE) as f:
try:
data = json.load(f)
print(f"Loaded {len(data)} records from {DATA_FILE}.")
return data
except:
print(f"Error loading {DATA_FILE}. File may be empty or corrupted.")
return []
@auth.require_admin
def save_data(data):
with open(DATA_FILE, 'w') as f:
json.dump(data, f, indent=4)
@auth.require_admin @auth.require_admin
def folder_secret_config_editor(): def folder_secret_config_editor():
return render_template('folder_secret_config_editor.html', alphabet=ALPHABET, admin_enabled=auth.is_admin()) return render_template('folder_secret_config_editor.html', alphabet=ALPHABET, admin_enabled=auth.is_admin())
@auth.require_admin
def folder_secret_config_data():
return jsonify(load_data())
@auth.require_admin @auth.require_admin
def folder_secret_config_action(): def folder_secret_config_action():
p = request.get_json() p = request.get_json()
data = load_data() data = auth.return_folder_config()
action = p.get('action') action = p.get('action')
if action == 'delete': if action == 'delete':
data = [r for r in data if r['secret'] != p['secret']] data = [r for r in data if r['secret'] != p['secret']]
@ -58,6 +37,6 @@ def folder_secret_config_action():
'validity': datetime.strptime(p['validity'], '%Y-%m-%d').strftime('%d.%m.%Y'), 'validity': datetime.strptime(p['validity'], '%Y-%m-%d').strftime('%d.%m.%Y'),
'folders': p['folders'] 'folders': p['folders']
}) })
save_data(data) auth.save_folder_config(data)
return jsonify(success=True) return jsonify(success=True)

View File

@ -43,16 +43,18 @@
<h1>{{ title_long }}</h1> <h1>{{ title_long }}</h1>
</header> </header>
<div class="wrapper"> <div class="wrapper">
{% if admin_enabled %}
<div style="color: #ccc; border: 1px solid #ccc; padding: 10px; background-color: #979797;">
<a href="{{ url_for('mylinks') }}" class="btn" style="color: #ccc; text-decoration: none;">Meine Links</a>
<span> | </span>
<a href="{{ url_for('connections') }}" class="btn" style="color: #ccc; text-decoration: none;">Verbindungen</a>
<span> | </span>
<a href="{{ url_for('dashboard') }}" class="btn" style="color: #ccc; text-decoration: none;">Auswertung</a>
<span> | </span>
<a href="{{ url_for('folder_secret_config_editor') }}" class="btn" style="color: #ccc; text-decoration: none;" id="edit-folder-config" >Ordnerkonfiguration</a>
</div>
{% endif %}
<div class="container"> <div class="container">
{% if admin_enabled %}
<div style="margin: 20px;">
<a href="{{ url_for('index') }}" style="color: #ccc; text-decoration: none;">App</a><span style="color: #ccc;"> · </span>
<a href="{{ url_for('mylinks') }}" style="color: #ccc; text-decoration: none;">Meine Links</a><span style="color: #ccc;"> · </span>
<a href="{{ url_for('connections') }}" style="color: #ccc; text-decoration: none;">Verbindungen</a><span style="color: #ccc;"> · </span>
<a href="{{ url_for('dashboard') }}" style="color: #ccc; text-decoration: none;">Auswertung</a><span style="color: #ccc;"> · </span>
<a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config" style="color: #ccc; text-decoration: none;">Folder Config Editor</a>
</div>
{% endif %}
<div id="breadcrumbs" class="breadcrumb"></div> <div id="breadcrumbs" class="breadcrumb"></div>
<div id="content"></div> <div id="content"></div>
<div id="directory-controls"> <div id="directory-controls">

View File

@ -6,10 +6,7 @@
<title>{% block title %}Meine Links{% endblock %}</title> <title>{% block title %}Meine Links{% endblock %}</title>
<link <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<style> <style>
/* common styles */ /* common styles */
@ -31,33 +28,26 @@
.card { .card {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.locked { background-color:rgb(255, 255, 255); } .locked { background-color:#eee; }
.unlocked { background-color: #fff3cd; } .unlocked { background-color: #fff3cd; }
</style> </style>
{% block head_extra %}{% endblock %} {% block head_extra %}{% endblock %}
</head> </head>
<body> <body>
<script src="{{ url_for('static', filename='functions.js') }}"></script>
<!-- Navigation Bar --> <!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-3"> <div style="background-color: #979797; padding: 5px; display: flex; gap: 5px; flex-wrap: wrap;">
<div class="container-fluid"> <a href="{{ url_for('index') }}" class="btn btn-secondary btn-sm">App</a>
<div class="navbar-collapse" id="navbarNav"> <a href="{{ url_for('mylinks') }}" class="btn btn-secondary btn-sm">Meine Links</a>
<ul class="navbar-nav ms-auto"> <a href="{{ url_for('connections') }}" class="btn btn-secondary btn-sm">Verbindungen</a>
<li class="nav-item"><a href="{{ url_for('index') }}" class="nav-link">App</a></li> <a href="{{ url_for('dashboard') }}" class="btn btn-secondary btn-sm">Auswertung-Downloads</a>
<li class="nav-item"><a href="{{ url_for('mylinks') }}" class="nav-link">Meine Links</a></li> <a href="{{ url_for('songs_dashboard') }}" class="btn btn-secondary btn-sm">Auswertung-Wiederholungen</a>
<li class="nav-item"><a href="{{ url_for('connections') }}" class="nav-link">Verbindungen</a></li> {% if admin_enabled %}
<li class="nav-item"><a href="{{ url_for('dashboard') }}" class="nav-link">Auswertung-Downloads</a></li> <a href="{{ url_for('folder_secret_config_editor') }}" class="btn btn-secondary btn-sm" id="edit-folder-config">Ordnerkonfiguration</a>
<li class="nav-item"><a href="{{ url_for('songs_dashboard') }}" class="nav-link">Auswertung-Wiederholungen</a></li> {% endif %}
{% if admin_enabled %} {% block nav_extra %}{% endblock %}
<li class="nav-item"><a href="{{ url_for('folder_secret_config_editor') }}" class="nav-link">Edit Folder Config</a></li> </div>
{% endif %}
</ul>
</div>
{% block nav_extra %}{% endblock %}
</div>
</nav>
<div class="container"> <div class="container">
<h2>{% block nav_brand %}Übersicht deiner gültigen Links{% endblock %}</h2> <h2>{% block nav_brand %}Übersicht deiner gültigen Links{% endblock %}</h2>
</div> </div>
@ -67,5 +57,6 @@
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='functions.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -145,7 +145,7 @@
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Anzahl Nutzer</h5> <h5 class="card-title">Anzahl Geräte</h5>
<canvas id="distinctDeviceChart"></canvas> <canvas id="distinctDeviceChart"></canvas>
</div> </div>
</div> </div>
@ -246,7 +246,6 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script> <script>
// Data passed from the backend as JSON // Data passed from the backend as JSON

View File

@ -2,10 +2,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{# page title #} {# page title #}
{% block title %}Edit Folder Config{% endblock %} {% block title %}Ordnerkonfiguration{% endblock %}
{# override navbar text: #} {# override navbar text: #}
{% block nav_brand %}Folder Config Editor{% endblock %} {% block nav_brand %}Ordnerkonfiguration{% endblock %}
{# page content #} {# page content #}
{% block content %} {% block content %}
@ -35,161 +35,283 @@
{% endblock %} {% endblock %}
{% block scripts %} {% 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> <script>
const ALPHABET = {{ alphabet|tojson }}; const ALPHABET = {{ alphabet|tojson }};
let data = []; let data = [];
let editing = new Set(); let editing = new Set();
let pendingDelete = null; // store secret pending confirmation let pendingDelete = null;
function loadData() { // helper to format DD.MM.YYYY → YYYY-MM-DD
$.getJSON('/admin/folder_secret_config_editor/data', res => { data = res; render(); }); 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() { function render() {
const $cont = $('#records').empty(); const cont = document.getElementById('records');
cont.innerHTML = '';
data.forEach(rec => { data.forEach(rec => {
const key = rec.secret; const key = rec.secret;
const isEdit = editing.has(key); const isEdit = editing.has(key);
const expired = (formatISO(rec.validity) && new Date(formatISO(rec.validity)) < new Date());
const cls = isEdit ? 'unlocked' : 'locked'; const cls = isEdit ? 'unlocked' : 'locked';
// Determine if entry has expired // outer card
const validityISO = formatISO(rec.validity); const wrapper = document.createElement('div');
const expired = validityISO ? (new Date(validityISO) < new Date()) : false; wrapper.className = 'card mb-3';
wrapper.dataset.secret = key;
// Build card HTML const body = document.createElement('div');
let html = `<div class="card mb-3 ${cls}" data-secret="${key}"><div class="card-body">`; body.className = `card-body ${cls}`;
// Show exclamation if expired
html += `<h5>Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}</h5>`; // header
html += `<div class="mb-2">Secret: <input class="form-control" type="text" value="${rec.secret}" ${isEdit?'':'readonly'} data-field="secret"></div>`; const h5 = document.createElement('h5');
html += `<div class="mb-2">Validity: <input class="form-control" type="date" value="${validityISO}" ${isEdit?'':'readonly'} data-field="validity"></div>`; h5.innerHTML = `Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}`;
html += `<h6>Ordner</h6>`; body.appendChild(h5);
rec.folders.forEach((f,i) => {
html += `<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px;">`; // secret input
html += `Ordnername: <div class="input-group mb-2">`; const secDiv = document.createElement('div');
html += `<input class="form-control" type="text" value="${f.foldername}" ${isEdit?'':'readonly'} data-field="foldername-${i}">`; secDiv.className = 'mb-2';
if(isEdit) html += `<button class="btn btn-outline-danger" onclick="removeFolder('${key}',${i})">Remove</button>`; secDiv.innerHTML = `Secret: `;
html += `</div>`; const secInput = document.createElement('input');
html += `Ordnerpfad: <input class="form-control mb-2" type="text" value="${f.folderpath}" ${isEdit?'':'readonly'} data-field="folderpath-${i}">`; secInput.className = 'form-control';
html += `</div>`; 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 = !isEdit;
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 = 'Remove';
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) html += `<button class="btn btn-sm btn-primary mb-2" onclick="addFolder('${key}')">Add Folder</button>`;
html += `<div>`; if (isEdit) {
if (!isEdit) html += `<a class="btn btn-secondary btn-sm me-2" href="/?secret=${rec.secret}">Link öffnen</a>`; const addFld = document.createElement('button');
html += `<button class="btn btn-danger btn-sm me-2 delete-btn" data-secret="${key}">Delete</button>`; addFld.className = 'btn btn-sm btn-primary mb-2';
html += `<button class="btn btn-secondary btn-sm me-2" onclick="cloneRec('${key}')">Clone</button>`; addFld.type = 'button';
if(isEdit) html += `<button class="btn btn-success btn-sm" onclick="saveRec('${key}')">Save</button>`; addFld.textContent = 'Add Folder';
else html += `<button class="btn btn-warning btn-sm" onclick="editRec('${key}')">Edit</button>`; addFld.addEventListener('click', () => addFolder(key));
html += `</div></div></div>`; body.appendChild(addFld);
$cont.append(html); }
// actions row
const actions = document.createElement('div');
if (!isEdit) {
const openLink = document.createElement('a');
openLink.className = 'btn btn-secondary btn-sm me-2';
openLink.href = `/?secret=${rec.secret}`;
openLink.textContent = 'Link öffnen';
actions.appendChild(openLink);
}
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-danger btn-sm me-2 delete-btn';
delBtn.type = 'button';
delBtn.textContent = 'Delete';
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 = 'Clone';
cloneBtn.addEventListener('click', () => cloneRec(key));
actions.appendChild(cloneBtn);
if (isEdit) {
const saveBtn = document.createElement('button');
saveBtn.className = 'btn btn-success btn-sm';
saveBtn.type = 'button';
saveBtn.textContent = 'Save';
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 = 'Edit';
editBtn.addEventListener('click', () => editRec(key));
actions.appendChild(editBtn);
}
body.appendChild(actions);
wrapper.appendChild(body);
cont.appendChild(wrapper);
}); });
} }
function formatISO(d) { // generate a unique secret
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) { function generateSecret(existing) {
let s; let s;
do { do {
s = Array.from({length:32},_=>ALPHABET[Math.floor(Math.random()*ALPHABET.length)]).join(''); s = Array.from({length:32}, () => ALPHABET[Math.floor(Math.random()*ALPHABET.length)]).join('');
} while(existing.includes(s)); } while (existing.includes(s));
return s; return s;
} }
$(document).ready(loadData); // CRUD helpers
$('#add-btn').click(() => { function editRec(secret) {
const existing = data.map(r=>r.secret); editing.add(secret);
const newSecret = generateSecret(existing);
data.push({secret:newSecret, validity:new Date().toISOString().substr(0,10), folders:[]});
editing.add(newSecret);
render(); render();
}
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);
data.splice(idx+1, 0, rec);
editing.add(rec.secret);
render();
}
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();
});
loadData();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -27,7 +27,7 @@
<p class="card-text"> <p class="card-text">
<small class="text-muted">Gültig bis: {{ secret_valid_to[secret] }}</small> <small class="text-muted">Gültig bis: {{ secret_valid_to[secret] }}</small>
</p> </p>
<a href="{{ secret_url[secret] }}" class="btn btn-secondary btn-sm">Link öffnen</a> <a href="{{ secret_url[secret] }}" class="btn btn-secondary btn-sm" style="text-decoration: none;">Link öffnen</a>
<button class="btn btn-secondary btn-sm" <button class="btn btn-secondary btn-sm"
onclick="toClipboard('{{ secret_url[secret] }}')"> onclick="toClipboard('{{ secret_url[secret] }}')">
Link kopieren Link kopieren

View File

@ -33,8 +33,8 @@
<div class="card-body"> <div class="card-body">
<div class="card-text mt-2"> <div class="card-text mt-2">
<h2 class="card-title">Token-Link:</h2> <h2 class="card-title">Token-Link:</h2>
<a href="{{ token_url }}" class="copy-btn" id="tokenLink">Direct Link</a> <a href="{{ token_url }}" class="copy-btn" style="text-decoration: none;" id="tokenLink">Link öffnen</a>
<button class="copy-btn" onclick="toClipboard('{{ token_url }}')">Copy Link</button> <button class="copy-btn" onclick="toClipboard('{{ token_url }}')">Link kopieren</button>
</div> </div>
<div class="card-text mt-2"> <div class="card-text mt-2">
<h2 class="mt-3">Gültig bis:</h2> <h2 class="mt-3">Gültig bis:</h2>