Add optional DJ panel password

This commit is contained in:
3nd3r
2026-01-03 09:12:57 -06:00
parent 2db40e4547
commit 95b01fd436
4 changed files with 145 additions and 0 deletions

3
.gitignore vendored
View File

@@ -15,6 +15,9 @@ ENV/
.env
.env.*
# Local config (may contain secrets)
config.json
# Tooling caches
.pytest_cache/
.mypy_cache/

View File

@@ -85,6 +85,23 @@ YOUTUBE_API_KEY=YOUR_KEY_HERE
Notes:
- If you dont 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 youre 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://<DJ_MACHINE_IP>:5000` shows a login prompt.
- Listener (`:5001`) is not affected.
---
## Run

3
config.example.json Normal file
View File

@@ -0,0 +1,3 @@
{
"dj_panel_password": ""
}

122
server.py
View File

@@ -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 (
"<!doctype html><html><head><meta http-equiv='refresh' content='0; url=/login' /></head>"
"<body>Redirecting to <a href='/login'>/login</a>...</body></html>",
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 (
"<!doctype html><html><head><meta http-equiv='refresh' content='0; url=/' /></head>"
"<body>Auth disabled. Redirecting...</body></html>",
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 (
"<!doctype html><html><head><meta http-equiv='refresh' content='0; url=/' /></head>"
"<body>Logged in. Redirecting...</body></html>",
302,
{'Location': '/'}
)
error = 'Invalid password'
# Minimal inline login page (no new assets)
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>TechDJ - DJ Login</title>
<style>
body {{ background:#0a0a12; color:#eee; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin:0; }}
.wrap {{ min-height:100vh; display:flex; align-items:center; justify-content:center; padding:24px; }}
.card {{ width:100%; max-width:420px; background:rgba(10,10,20,0.85); border:2px solid #bc13fe; border-radius:16px; padding:24px; box-shadow:0 0 40px rgba(188,19,254,0.25); }}
h1 {{ margin:0 0 16px 0; font-size:22px; }}
label {{ display:block; margin:12px 0 8px; opacity:0.9; }}
input {{ width:100%; padding:12px; border-radius:10px; border:1px solid rgba(255,255,255,0.15); background:rgba(0,0,0,0.35); color:#fff; }}
button {{ width:100%; margin-top:14px; padding:12px; border-radius:10px; border:2px solid #bc13fe; background:rgba(188,19,254,0.15); color:#fff; font-weight:700; cursor:pointer; }}
.err {{ margin-top:12px; color:#ffb3ff; }}
.hint {{ margin-top:10px; font-size:12px; opacity:0.7; }}
</style>
</head>
<body>
<div class=\"wrap\">
<div class=\"card\">
<h1>DJ Panel Locked</h1>
<form method=\"post\" action=\"/login\">
<label for=\"password\">Password</label>
<input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" autofocus />
<button type=\"submit\">Unlock DJ Panel</button>
{f"<div class='err'>{error}</div>" if error else ""}
<div class=\"hint\">Set/disable this in config.json (dj_panel_password).</div>
</form>
</div>
</div>
</body>
</html>"""
@dj_app.route('/logout')
def dj_logout():
session.pop('dj_authed', None)
return (
"<!doctype html><html><head><meta http-equiv='refresh' content='0; url=/login' /></head>"
"<body>Logged out. Redirecting...</body></html>",
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)