Compare commits

..

No commits in common. "9cbb58417e85f3ac6f0845b398a7884de17e1935" and "80e3b1abb5181f315f35c696413feb21c73584f7" have entirely different histories.

9 changed files with 210 additions and 332 deletions

12
app.py
View File

@ -9,6 +9,7 @@ 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
@ -23,7 +24,11 @@ import auth
import analytics as a import analytics as a
import folder_secret_config_editor as fsce import folder_secret_config_editor as fsce
app_config = auth.return_app_config() with open("app_config.json", 'r') as file:
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)
@ -50,7 +55,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(auth.load_folder_config)) 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/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
@ -99,7 +104,6 @@ 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)
@ -437,7 +441,6 @@ def create_share(subpath):
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']:
@ -547,7 +550,6 @@ 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,42 +12,29 @@ 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_FILENAME, 'r') as file: with open("app_config.json", 'r') as file:
app_config = json.load(file) app_config = json.load(file)
with open(FOLDER_CONFIG_FILENAME) as file: with open('folder_secret_config.json') 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)
@ -163,24 +150,6 @@ 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,18 +7,39 @@ 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 = auth.return_folder_config() data = load_data()
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']]
@ -37,6 +58,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']
}) })
auth.save_folder_config(data) save_data(data)
return jsonify(success=True) return jsonify(success=True)

View File

@ -43,18 +43,16 @@
<h1>{{ title_long }}</h1> <h1>{{ title_long }}</h1>
</header> </header>
<div class="wrapper"> <div class="wrapper">
<div class="container">
{% if admin_enabled %} {% if admin_enabled %}
<div style="color: #ccc; border: 1px solid #ccc; padding: 10px; background-color: #979797;"> <div style="margin: 20px;">
<a href="{{ url_for('mylinks') }}" class="btn" style="color: #ccc; text-decoration: none;">Meine Links</a> <a href="{{ url_for('index') }}" style="color: #ccc; text-decoration: none;">App</a><span style="color: #ccc;"> · </span>
<span> | </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') }}" class="btn" style="color: #ccc; text-decoration: none;">Verbindungen</a> <a href="{{ url_for('connections') }}" style="color: #ccc; text-decoration: none;">Verbindungen</a><span style="color: #ccc;"> · </span>
<span> | </span> <a href="{{ url_for('dashboard') }}" style="color: #ccc; text-decoration: none;">Auswertung</a><span style="color: #ccc;"> · </span>
<a href="{{ url_for('dashboard') }}" class="btn" style="color: #ccc; text-decoration: none;">Auswertung</a> <a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config" style="color: #ccc; text-decoration: none;">Folder Config Editor</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> </div>
{% endif %} {% endif %}
<div class="container">
<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,7 +6,10 @@
<title>{% block title %}Meine Links{% endblock %}</title> <title>{% block title %}Meine Links{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<style> <style>
/* common styles */ /* common styles */
@ -28,26 +31,33 @@
.card { .card {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.locked { background-color:#eee; } .locked { background-color:rgb(255, 255, 255); }
.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 -->
<div style="background-color: #979797; padding: 5px; display: flex; gap: 5px; flex-wrap: wrap;"> <nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-3">
<a href="{{ url_for('index') }}" class="btn btn-secondary btn-sm">App</a> <div class="container-fluid">
<a href="{{ url_for('mylinks') }}" class="btn btn-secondary btn-sm">Meine Links</a> <div class="navbar-collapse" id="navbarNav">
<a href="{{ url_for('connections') }}" class="btn btn-secondary btn-sm">Verbindungen</a> <ul class="navbar-nav ms-auto">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary btn-sm">Auswertung-Downloads</a> <li class="nav-item"><a href="{{ url_for('index') }}" class="nav-link">App</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('mylinks') }}" class="nav-link">Meine Links</a></li>
<li class="nav-item"><a href="{{ url_for('connections') }}" class="nav-link">Verbindungen</a></li>
<li class="nav-item"><a href="{{ url_for('dashboard') }}" class="nav-link">Auswertung-Downloads</a></li>
<li class="nav-item"><a href="{{ url_for('songs_dashboard') }}" class="nav-link">Auswertung-Wiederholungen</a></li>
{% if admin_enabled %} {% if admin_enabled %}
<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('folder_secret_config_editor') }}" class="nav-link">Edit Folder Config</a></li>
{% endif %} {% endif %}
</ul>
</div>
{% block nav_extra %}{% endblock %} {% block nav_extra %}{% endblock %}
</div> </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>
@ -57,6 +67,5 @@
{% 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 Geräte</h5> <h5 class="card-title">Anzahl Nutzer</h5>
<canvas id="distinctDeviceChart"></canvas> <canvas id="distinctDeviceChart"></canvas>
</div> </div>
</div> </div>
@ -246,6 +246,7 @@
{% 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 %}Ordnerkonfiguration{% endblock %} {% block title %}Edit Folder Config{% endblock %}
{# override navbar text: #} {# override navbar text: #}
{% block nav_brand %}Ordnerkonfiguration{% endblock %} {% block nav_brand %}Folder Config Editor{% endblock %}
{# page content #} {# page content #}
{% block content %} {% block content %}
@ -35,283 +35,161 @@
{% 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; let pendingDelete = null; // store secret pending confirmation
// helper to format DD.MM.YYYY → YYYY-MM-DD function loadData() {
function formatISO(d) { $.getJSON('/admin/folder_secret_config_editor/data', res => { data = res; render(); });
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 = document.getElementById('records'); const $cont = $('#records').empty();
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';
// outer card // Determine if entry has expired
const wrapper = document.createElement('div'); const validityISO = formatISO(rec.validity);
wrapper.className = 'card mb-3'; const expired = validityISO ? (new Date(validityISO) < new Date()) : false;
wrapper.dataset.secret = key;
const body = document.createElement('div');
body.className = `card-body ${cls}`;
// header
const h5 = document.createElement('h5');
h5.innerHTML = `Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}`;
body.appendChild(h5);
// 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 = !isEdit;
valInput.dataset.field = 'validity';
valDiv.appendChild(valInput);
body.appendChild(valDiv);
// folders
const folderHeader = document.createElement('h6');
folderHeader.textContent = 'Ordner';
body.appendChild(folderHeader);
// 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) => { rec.folders.forEach((f,i) => {
const fwrap = document.createElement('div'); html += `<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px;">`;
fwrap.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}">`;
// name if(isEdit) html += `<button class="btn btn-outline-danger" onclick="removeFolder('${key}',${i})">Remove</button>`;
fwrap.appendChild(document.createTextNode("Ordnername: ")); html += `</div>`;
const nameGroup = document.createElement('div'); html += `Ordnerpfad: <input class="form-control mb-2" type="text" value="${f.folderpath}" ${isEdit?'':'readonly'} data-field="folderpath-${i}">`;
nameGroup.className = 'input-group mb-2'; html += `</div>`;
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>`;
if (isEdit) { html += `<div>`;
const addFld = document.createElement('button'); if (!isEdit) html += `<a class="btn btn-secondary btn-sm me-2" href="/?secret=${rec.secret}">Link öffnen</a>`;
addFld.className = 'btn btn-sm btn-primary mb-2'; html += `<button class="btn btn-danger btn-sm me-2 delete-btn" data-secret="${key}">Delete</button>`;
addFld.type = 'button'; html += `<button class="btn btn-secondary btn-sm me-2" onclick="cloneRec('${key}')">Clone</button>`;
addFld.textContent = 'Add Folder'; if(isEdit) html += `<button class="btn btn-success btn-sm" onclick="saveRec('${key}')">Save</button>`;
addFld.addEventListener('click', () => addFolder(key)); else html += `<button class="btn btn-warning btn-sm" onclick="editRec('${key}')">Edit</button>`;
body.appendChild(addFld); html += `</div></div></div>`;
} $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);
}); });
} }
// generate a unique secret function formatISO(d) {
function generateSecret(existing) { const p = d.split('.');
let s; return p.length===3 ? `${p[2]}-${p[1]}-${p[0]}` : '';
do {
s = Array.from({length:32}, () => ALPHABET[Math.floor(Math.random()*ALPHABET.length)]).join('');
} while (existing.includes(s));
return s;
} }
// CRUD helpers // original delete function, called after confirmation
function editRec(secret) { function deleteRec(secret) {
editing.add(secret); $.ajax({
render(); 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) { function cloneRec(secret) {
const idx = data.findIndex(r => r.secret === secret); const rec = data.find(r=>r.secret===secret);
const rec = JSON.parse(JSON.stringify(data[idx]));
const existing = data.map(r=>r.secret); const existing = data.map(r=>r.secret);
rec.secret = generateSecret(existing); const clone = JSON.parse(JSON.stringify(rec));
data.splice(idx+1, 0, rec); clone.secret = generateSecret(existing);
editing.add(rec.secret); data.splice(data.findIndex(r=>r.secret===secret)+1, 0, clone);
editing.add(clone.secret);
render(); render();
} }
function addFolder(secret) { function addFolder(secret) {
data.find(r => r.secret === secret).folders.push({foldername:'', folderpath:''}); const rec = data.find(r=>r.secret===secret);
render(); rec.folders.push({foldername:'',folderpath:''}); render();
} }
function removeFolder(secret,i) { 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 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) => ({ const folders = rec.folders.map((_,i) => ({
foldername: card.querySelector(`input[data-field="foldername-${i}"]`).value.trim(), foldername: $card.find(`[data-field=foldername-${i}]`).val(),
folderpath: card.querySelector(`input[data-field="folderpath-${i}"]`).value.trim() folderpath: $card.find(`[data-field=folderpath-${i}]`).val()
})); }));
if(!newSecret||data.some(r=>r.secret!==secret&&r.secret===newSecret)){ if(!newSecret||data.some(r=>r.secret!==secret&&r.secret===newSecret)){
return alert('Secret must be unique and non-empty'); alert('Secret must be unique and non-empty');
return;
} }
if(!validity){ if(!validity){
return alert('Validity required'); alert('Validity required');
return;
} }
if(folders.some(f=>!f.foldername||!f.folderpath)){ if(folders.some(f=>!f.foldername||!f.folderpath)){
return alert('Folder entries cannot be empty'); alert('Folder entries cannot be empty');
return;
} }
$.ajax({
await sendAction({ url:'/admin/folder_secret_config_editor/action',
method:'POST',
contentType:'application/json',
data: JSON.stringify({
action:'update', action:'update',
oldSecret:secret, oldSecret:secret,
newSecret, newSecret,
validity, validity,
folders folders
}); })
}).always(()=>{
editing.delete(secret); editing.delete(secret);
await loadData(); 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;
} }
// modal handling for delete $(document).ready(loadData);
document.addEventListener('click', e => { $('#add-btn').click(() => {
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 existing = data.map(r=>r.secret);
const newSecret = generateSecret(existing); const newSecret = generateSecret(existing);
data.push({ secret: newSecret, validity: new Date().toISOString().slice(0,10), folders: [] }); data.push({secret:newSecret, validity:new Date().toISOString().substr(0,10), folders:[]});
editing.add(newSecret); editing.add(newSecret);
render(); 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" style="text-decoration: none;">Link öffnen</a> <a href="{{ secret_url[secret] }}" class="btn btn-secondary btn-sm">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" style="text-decoration: none;" id="tokenLink">Link öffnen</a> <a href="{{ token_url }}" class="copy-btn" id="tokenLink">Direct Link</a>
<button class="copy-btn" onclick="toClipboard('{{ token_url }}')">Link kopieren</button> <button class="copy-btn" onclick="toClipboard('{{ token_url }}')">Copy Link</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>