Compare commits
No commits in common. "9cbb58417e85f3ac6f0845b398a7884de17e1935" and "80e3b1abb5181f315f35c696413feb21c73584f7" have entirely different histories.
9cbb58417e
...
80e3b1abb5
14
app.py
14
app.py
@ -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)
|
||||||
|
|
||||||
@ -434,10 +438,9 @@ 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']:
|
||||||
@ -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
55
auth.py
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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');
|
// Build card HTML
|
||||||
body.className = `card-body ${cls}`;
|
let html = `<div class="card mb-3 ${cls}" data-secret="${key}"><div class="card-body">`;
|
||||||
|
// Show exclamation if expired
|
||||||
// header
|
html += `<h5>Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}</h5>`;
|
||||||
const h5 = document.createElement('h5');
|
html += `<div class="mb-2">Secret: <input class="form-control" type="text" value="${rec.secret}" ${isEdit?'':'readonly'} data-field="secret"></div>`;
|
||||||
h5.innerHTML = `Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}`;
|
html += `<div class="mb-2">Validity: <input class="form-control" type="date" value="${validityISO}" ${isEdit?'':'readonly'} data-field="validity"></div>`;
|
||||||
body.appendChild(h5);
|
html += `<h6>Ordner</h6>`;
|
||||||
|
rec.folders.forEach((f,i) => {
|
||||||
// secret input
|
html += `<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px;">`;
|
||||||
const secDiv = document.createElement('div');
|
html += `Ordnername: <div class="input-group mb-2">`;
|
||||||
secDiv.className = 'mb-2';
|
html += `<input class="form-control" type="text" value="${f.foldername}" ${isEdit?'':'readonly'} data-field="foldername-${i}">`;
|
||||||
secDiv.innerHTML = `Secret: `;
|
if(isEdit) html += `<button class="btn btn-outline-danger" onclick="removeFolder('${key}',${i})">Remove</button>`;
|
||||||
const secInput = document.createElement('input');
|
html += `</div>`;
|
||||||
secInput.className = 'form-control';
|
html += `Ordnerpfad: <input class="form-control mb-2" type="text" value="${f.folderpath}" ${isEdit?'':'readonly'} data-field="folderpath-${i}">`;
|
||||||
secInput.type = 'text';
|
html += `</div>`;
|
||||||
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>`;
|
||||||
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',
|
||||||
function cloneRec(secret) {
|
contentType: 'application/json',
|
||||||
const idx = data.findIndex(r => r.secret === secret);
|
data: JSON.stringify({ action: 'delete', secret })
|
||||||
const rec = JSON.parse(JSON.stringify(data[idx]));
|
}).always(loadData);
|
||||||
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) {
|
// hook delete buttons to show modal
|
||||||
const card = document.querySelector(`[data-secret="${secret}"]`);
|
$(document).on('click', '.delete-btn', function() {
|
||||||
const newSecret = card.querySelector('input[data-field="secret"]').value.trim();
|
pendingDelete = $(this).data('secret');
|
||||||
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'));
|
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.querySelector('.confirm-delete').addEventListener('click', () => {
|
|
||||||
if (pendingDelete) deleteRec(pendingDelete);
|
// when user confirms deletion
|
||||||
|
$('.confirm-delete').on('click', function() {
|
||||||
|
if (pendingDelete) {
|
||||||
|
deleteRec(pendingDelete);
|
||||||
pendingDelete = null;
|
pendingDelete = null;
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal'));
|
}
|
||||||
|
const modalEl = document.getElementById('confirmDeleteModal');
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
modal.hide();
|
modal.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
// init
|
function editRec(secret) { editing.add(secret); render(); }
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
function cloneRec(secret) {
|
||||||
document.getElementById('add-btn').addEventListener('click', () => {
|
const rec = data.find(r=>r.secret===secret);
|
||||||
const existing = data.map(r => r.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);
|
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 %}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user