diff --git a/.gitignore b/.gitignore index 4f001c3..4ef34e7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ ENV/ .env .env.* +# Local config (may contain secrets) +config.json + # Tooling caches .pytest_cache/ .mypy_cache/ diff --git a/README.md b/README.md index c6a4875..55dcb4b 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,23 @@ YOUTUBE_API_KEY=YOUR_KEY_HERE Notes: - If you don’t set `YOUTUBE_API_KEY`, you can still paste a YouTube URL directly into a deck/download box. +## Optional DJ panel password (config.json) + +By default, anyone who can reach the DJ server (`:5000`) can open the DJ panel. + +If you want to lock it while you’re playing live, create a `config.json` (not committed) in the project root: + +```json +{ + "dj_panel_password": "your-strong-password" +} +``` + +Behavior: +- If `dj_panel_password` is empty/missing, the DJ panel is **unlocked** (default). +- If set, visiting `http://:5000` shows a login prompt. +- Listener (`:5001`) is not affected. + --- ## Run diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..15491b5 --- /dev/null +++ b/config.example.json @@ -0,0 +1,3 @@ +{ + "dj_panel_password": "" +} diff --git a/server.py b/server.py index e95136c..142104d 100644 --- a/server.py +++ b/server.py @@ -3,6 +3,7 @@ import eventlet eventlet.monkey_patch() import os +import json import subprocess import threading import queue @@ -14,6 +15,26 @@ from dotenv import load_dotenv load_dotenv() import downloader + +def _load_config(): + """Loads optional config.json from the project root. + + If missing or invalid, returns an empty dict. + """ + try: + with open('config.json', 'r', encoding='utf-8') as f: + data = json.load(f) + return data if isinstance(data, dict) else {} + except FileNotFoundError: + return {} + except Exception: + return {} + + +CONFIG = _load_config() +DJ_PANEL_PASSWORD = (CONFIG.get('dj_panel_password') or '').strip() +DJ_AUTH_ENABLED = bool(DJ_PANEL_PASSWORD) + # Relay State broadcast_state = { 'active': False, @@ -311,6 +332,104 @@ def setup_shared_routes(app): dj_app = Flask(__name__, static_folder='.', static_url_path='') dj_app.config['SECRET_KEY'] = 'dj_panel_secret' setup_shared_routes(dj_app) + + +@dj_app.before_request +def _protect_dj_panel(): + """Optionally require a password for the DJ panel only (port 5000). + + This does not affect the listener server (port 5001). + """ + if not DJ_AUTH_ENABLED: + return None + + # Allow login/logout endpoints + if request.path in ('/login', '/logout'): + return None + + # If already authenticated, allow + if session.get('dj_authed') is True: + return None + + # Redirect everything else to login + return ( + "" + "Redirecting to /login...", + 302, + {'Location': '/login'} + ) + + +@dj_app.route('/login', methods=['GET', 'POST']) +def dj_login(): + if not DJ_AUTH_ENABLED: + # If auth is disabled, just go to the panel. + session['dj_authed'] = True + return ( + "" + "Auth disabled. Redirecting...", + 302, + {'Location': '/'} + ) + + error = None + if request.method == 'POST': + pw = (request.form.get('password') or '').strip() + if pw == DJ_PANEL_PASSWORD: + session['dj_authed'] = True + return ( + "" + "Logged in. Redirecting...", + 302, + {'Location': '/'} + ) + error = 'Invalid password' + + # Minimal inline login page (no new assets) + return f""" + + + + + TechDJ - DJ Login + + + +
+
+

DJ Panel Locked

+
+ + + + {f"
{error}
" if error else ""} +
Set/disable this in config.json (dj_panel_password).
+
+
+
+ +""" + + +@dj_app.route('/logout') +def dj_logout(): + session.pop('dj_authed', None) + return ( + "" + "Logged out. Redirecting...", + 302, + {'Location': '/login'} + ) dj_socketio = SocketIO( dj_app, cors_allowed_origins="*", @@ -324,6 +443,9 @@ dj_socketio = SocketIO( @dj_socketio.on('connect') def dj_connect(): + if DJ_AUTH_ENABLED and session.get('dj_authed') is not True: + print(f"⛔ DJ socket rejected (unauthorized): {request.sid}") + return False print(f"🎧 DJ connected: {request.sid}") dj_sids.add(request.sid)