diff --git a/.gitignore b/.gitignore index 26464f9..e12ff74 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ /search.db /access_log.db /access_log.db.bak -/folder_permission_config.json +/folder_secret_config.json /folder_mount_config.json /.env /app_config.json diff --git a/analytics.py b/analytics.py index d10d8dc..4d562aa 100644 --- a/analytics.py +++ b/analytics.py @@ -6,6 +6,7 @@ from auth import require_secret from collections import defaultdict import json import os +import auth file_access_temp = [] @@ -189,11 +190,11 @@ def songs_dashboard(): performance_data = [(count, titel) for titel, count in performance_counts.items()] performance_data.sort(reverse=True, key=lambda x: x[0]) - return render_template('songs_dashboard.html', timeframe=timeframe_param, performance_data=performance_data, site=site, category=category) + return render_template('songs_dashboard.html', timeframe=timeframe_param, performance_data=performance_data, site=site, category=category, admin_enabled=auth.is_admin()) @require_secret def connections(): - return render_template('connections.html') + return render_template('connections.html', admin_enabled=auth.is_admin()) @require_secret def dashboard(): @@ -476,7 +477,8 @@ def dashboard(): unique_files=unique_files, unique_user=unique_user, cached_percentage=cached_percentage, - timeframe_data=timeframe_data + timeframe_data=timeframe_data, + admin_enabled=auth.is_admin() ) def export_to_excel(): diff --git a/app.py b/app.py index a9a0e8a..e79b22f 100755 --- a/app.py +++ b/app.py @@ -9,7 +9,6 @@ import mimetypes from datetime import datetime, date, timedelta import diskcache import threading -import json import time from flask_socketio import SocketIO, emit import geoip2.database @@ -22,12 +21,9 @@ import base64 import search import auth import analytics as a +import folder_secret_config_editor as fsce -with open("app_config.json", 'r') as file: - app_config = json.load(file) - -with open('folder_permission_config.json') as file: - folder_config = json.load(file) +app_config = auth.return_app_config() 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) @@ -53,6 +49,9 @@ 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('/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/action', view_func=auth.require_admin(fsce.folder_secret_config_action), methods=['POST']) # Grab the HOST_RULE environment variable host_rule = os.getenv("HOST_RULE", "") @@ -100,6 +99,7 @@ def list_directory_contents(directory, subpath): """ directories = [] files = [] + folder_config = auth.return_folder_config() transcription_dir = os.path.join(directory, "Transkription") transcription_exists = os.path.isdir(transcription_dir) @@ -434,9 +434,10 @@ def get_transcript(subpath): def create_share(subpath): scheme = request.scheme # current scheme (http or https) 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 + folder_config = auth.return_folder_config() paths = {} for item in folder_config: for folder in item['folders']: @@ -546,16 +547,14 @@ def handle_request_initial_data(): @app.route('/') @auth.require_secret def index(path): + app_config = auth.return_app_config() title_short = app_config.get('TITLE_SHORT', 'Default Title') title_long = app_config.get('TITLE_LONG' , 'Default Title') - admin_enabled = False - if 'admin' in session: - admin_enabled = session['admin'] return render_template("app.html", title_short=title_short, title_long=title_long, - admin_enabled=admin_enabled, + admin_enabled=auth.is_admin() ) if __name__ == '__main__': diff --git a/auth.py b/auth.py index d0ed94c..01d27aa 100644 --- a/auth.py +++ b/auth.py @@ -12,14 +12,42 @@ import zlib import hmac import hashlib +FOLDER_CONFIG_FILENAME = 'folder_secret_config.json' +APP_CONFIG_FILENAME = 'app_config.json' - -with open("app_config.json", 'r') as file: +# initial read of the config files +with open(APP_CONFIG_FILENAME, 'r') as file: app_config = json.load(file) -with open('folder_permission_config.json') as file: +with open(FOLDER_CONFIG_FILENAME) as 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(): + """ + Check if the user is an admin based on the session. + """ + return session.get('admin', False) + def require_secret(f): @wraps(f) @@ -104,8 +132,10 @@ def require_secret(f): if args_admin and config_admin: if args_admin == config_admin: + print(f"Admin access granted with key: {args_admin}") session['admin'] = True else: + print(f"Admin access denied with key: {args_admin}") session['admin'] = False # 6) If we have folders, proceed; otherwise show index @@ -129,9 +159,28 @@ def require_secret(f): header_text_color=header_text_color, main_text_color=main_text_color, background_color=background_color, + admin_enabled=is_admin() ) 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 def mylinks(): scheme = request.scheme # current scheme (http or https) @@ -197,7 +246,8 @@ def mylinks(): token_qr_codes=token_qr_codes, token_folders=token_folders, token_url=token_url, - token_valid_to=token_valid_to + token_valid_to=token_valid_to, + admin_enabled=is_admin() ) diff --git a/folder_secret_config_editor.py b/folder_secret_config_editor.py new file mode 100644 index 0000000..7e93f2d --- /dev/null +++ b/folder_secret_config_editor.py @@ -0,0 +1,42 @@ +from flask import Flask, request, jsonify, render_template +import json +import os +from datetime import datetime +import secrets +import string +import auth + + +# Secret alphabet +ALPHABET = string.ascii_letters + string.digits + + +@auth.require_admin +def folder_secret_config_editor(): + return render_template('folder_secret_config_editor.html', alphabet=ALPHABET, admin_enabled=auth.is_admin()) + +@auth.require_admin +def folder_secret_config_action(): + p = request.get_json() + data = auth.return_folder_config() + action = p.get('action') + if action == 'delete': + data = [r for r in data if r['secret'] != p['secret']] + elif action == 'update': + old = p['oldSecret']; new = p['newSecret'] + for i, r in enumerate(data): + if r['secret'] == old: + r['secret'] = new + r['validity'] = datetime.strptime(p['validity'], '%Y-%m-%d').strftime('%d.%m.%Y') + r['folders'] = p['folders'] + break + else: + # append if not found + data.append({ + 'secret': new, + 'validity': datetime.strptime(p['validity'], '%Y-%m-%d').strftime('%d.%m.%Y'), + 'folders': p['folders'] + }) + auth.save_folder_config(data) + return jsonify(success=True) + diff --git a/templates/app.html b/templates/app.html index 767b99e..25c3dc5 100644 --- a/templates/app.html +++ b/templates/app.html @@ -43,6 +43,17 @@

{{ title_long }}

+ {% if admin_enabled %} +
+ Meine Links + | + Verbindungen + | + Auswertung + | + Ordnerkonfiguration +
+ {% endif %}
diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..5789a34 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,62 @@ + + + + + + + {% block title %}Meine Links{% endblock %} + + + + + + {% block head_extra %}{% endblock %} + + + + +
+ App + Meine Links + Verbindungen + Auswertung-Downloads + Auswertung-Wiederholungen + {% if admin_enabled %} + Ordnerkonfiguration + {% endif %} + {% block nav_extra %}{% endblock %} +
+
+

{% block nav_brand %}Übersicht deiner gültigen Links{% endblock %}

+
+ + {% block content %}{% endblock %} + + {% block scripts %}{% endblock %} + + + + + diff --git a/templates/connections.html b/templates/connections.html index 3573830..6daa255 100644 --- a/templates/connections.html +++ b/templates/connections.html @@ -1,218 +1,139 @@ - - - - - - Recent Connections - - - - - - +{# templates/connections.html #} +{% extends 'base.html' %} -
-
- - - - - - - - - - - - - - - -
TimestampLocationUser AgentFile PathFile SizeMIME-TypCached
-
+{% block title %}Recent Connections{% endblock %} + +{% block nav_brand %}Downloads der letzten 10 Minuten{% endblock %} + +{% block nav_extra %} + + Anzahl Verbindungen: 0 + +{% endblock %} + +{% block head_extra %} + +{% endblock %} + +{% block content %} +
+
+ + + + + + + + + + + + + + {# dynamically populated via Socket.IO #} + +
TimestampLocationUser AgentFile PathFile SizeMIME-TypCached
+
+{% endblock %} - - - + - - - + + tbody.innerHTML = ""; + tbody.appendChild(frag); + } + + function animateTableWithNewRow(data) { + const tbody = document.getElementById("connectionsTableBody"); + const currentTs = new Set([...tbody.children].map(r => r.dataset.timestamp)); + const newRecs = data.filter(r => !currentTs.has(r.timestamp)); + + if (newRecs.length) { + const temp = createRow(newRecs[0], false); + temp.style.visibility = "hidden"; + tbody.appendChild(temp); + const h = temp.getBoundingClientRect().height; + temp.remove(); + + tbody.style.transform = `translateY(${h}px)`; + setTimeout(() => { + updateTable(data); + tbody.style.transition = "none"; + tbody.style.transform = "translateY(0)"; + void tbody.offsetWidth; + tbody.style.transition = "transform 0.5s ease-out"; + }, 500); + } else { + updateTable(data); + } + } + + socket.on("recent_connections", data => { + updateStats(data); + animateTableWithNewRow(data); + }); + +{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index edfa580..52e7aa9 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,42 +1,14 @@ - - - - - - Dashboard - Verbindungsanalyse - - - - - - +{# templates/dashboard.html #} +{% extends 'base.html' %} + +{# page title #} +{% block title %}Dashboard{% endblock %} + +{# override navbar text: #} +{% block nav_brand %}Auswertung-Downloads{% endblock %} + +{# page content #} +{% block content %}
@@ -173,7 +145,7 @@
-
Anzahl Nutzer
+
Anzahl Geräte
@@ -271,9 +243,9 @@
+ {% endblock %} - - + {% block scripts %} - - +{% endblock %} diff --git a/templates/folder_secret_config_editor.html b/templates/folder_secret_config_editor.html new file mode 100644 index 0000000..1841232 --- /dev/null +++ b/templates/folder_secret_config_editor.html @@ -0,0 +1,317 @@ +{# templates/mylinks.html #} +{% extends 'base.html' %} + +{# page title #} +{% block title %}Ordnerkonfiguration{% endblock %} + +{# override navbar text: #} +{% block nav_brand %}Ordnerkonfiguration{% endblock %} + +{# page content #} +{% block content %} +
+
+ +
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/mylinks.html b/templates/mylinks.html index 9400509..b003fcf 100644 --- a/templates/mylinks.html +++ b/templates/mylinks.html @@ -1,123 +1,105 @@ - - - - - - Meine Links - - - - - - - +{# templates/mylinks.html #} +{% extends 'base.html' %} -
-
- {% if valid_secrets %} - {% for secret in valid_secrets %} -
-
- QR Code for secret -
-
Geheimnis: {{ secret }}
-

- Gültig bis: {{ secret_valid_to[secret] }} -

- Link öffnen - -
-
- - -
- {% if secret_folders[secret] %} -
Ordner
-
    - {% for folder in secret_folders[secret] %} -
  • - {{ folder.foldername }} -
  • - {% endfor %} -
- {% else %} -

Keine Ordner für dieses Gemeimnis hinterlegt.

- {% endif %} -
-
+{# page title #} +{% block title %}Meine Links{% endblock %} + +{# override navbar text: #} +{# +{% block nav_brand %} + Übersicht deiner gültigen Links +{% endblock %} +#} + +{# page‐specific content #} +{% block content %} +
+
+ {% if valid_secrets %} + {% for secret in valid_secrets %} +
+
+ QR Code for secret +
+
Geheimnis: {{ secret }}
+

+ Gültig bis: {{ secret_valid_to[secret] }} +

+ Link öffnen + +
+
+ + +
+ {% if secret_folders[secret] %} +
Ordner
+
    + {% for folder in secret_folders[secret] %} +
  • + {{ folder.foldername }} +
  • + {% endfor %} +
+ {% else %} +

Keine Ordner für dieses Gemeimnis hinterlegt.

+ {% endif %}
- {% endfor %} - {% endif %} - {% if valid_tokens %} - {% for token in valid_tokens %} -
-
- QR Code for token -
-
Token-Link:
-

- Gültig bis: {{ token_valid_to[token] }} -

- Link öffnen - -
-
- - -
- {% if token_folders[token] %} -
Ordner
-
    - {% for folder in token_folders[token] %} -
  • - {{ folder.foldername }} -
  • - {% endfor %} -
- {% else %} -

Keine Ordner für dieses Gemeimnis hinterlegt.

- {% endif %} -
-
-
- {% endfor %} - {% endif %} - {% if not valid_secrets and not valid_tokens %} - - {% endif %} +
+ {% endfor %} + {% endif %} + + {% if valid_tokens %} + {% for token in valid_tokens %} +
+
+ QR Code for token +
+
Token-Link:
+

+ Gültig bis: {{ token_valid_to[token] }} +

+ Link öffnen + +
+
+ + +
+ {% if token_folders[token] %} +
Ordner
+
    + {% for folder in token_folders[token] %} +
  • + {{ folder.foldername }} +
  • + {% endfor %} +
+ {% else %} +

Keine Ordner für dieses Gemeimnis hinterlegt.

+ {% endif %} +
+
+
+ {% endfor %} + {% endif %} + + {% if not valid_secrets and not valid_tokens %} + -
- - - + {% endif %} +
+
+{% endblock %} diff --git a/templates/songs_dashboard.html b/templates/songs_dashboard.html index 5a7936d..f5aa56e 100644 --- a/templates/songs_dashboard.html +++ b/templates/songs_dashboard.html @@ -1,42 +1,14 @@ - - - - - - Dashboard - Verbindungsanalyse - - - - - - +{# templates/songs_dashboard.html #} +{% extends 'base.html' %} + +{# page title #} +{% block title %}Edit Folder Config{% endblock %} + +{# override navbar text: #} +{% block nav_brand %}Dashboard - Analyse Wiederholungen{% endblock %} + +{# page content #} +{% block content %}
@@ -180,3 +152,4 @@ +{% endblock %} \ No newline at end of file diff --git a/templates/view_token.html b/templates/view_token.html index 457a28e..30446e4 100644 --- a/templates/view_token.html +++ b/templates/view_token.html @@ -33,8 +33,8 @@

Token-Link:

- Direct Link - + Link öffnen +

Gültig bis: