"""
rndrSBC - Production Web Management Dashboard & Secure API
Hardened server with first-run authentication, session validation, path traversal protection,
safe command execution, quiet hours scheduling, and multi-playlist management.
"""
import os
import sys
import json
import time
import io
import secrets
import logging
import subprocess
import threading
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
import urllib.parse
from PIL import Image
# Secure password hashing from Werkzeug
from werkzeug.security import generate_password_hash, check_password_hash
from core.paths import CONFIG_PATH, resolve
# Onboarding: QR claim-token flow + AP-mode provisioning
from server.onboarding import (
claim_url_for_token,
validate_claim_token,
consume_claim_token,
issue_claim_token,
invalidate_unclaimed_tokens,
onboarding_state,
ap_manager as onboarding_ap_manager,
)
logger = logging.getLogger("rndrSBC.server")
# In-memory active session tokens: {session_token: {"created_at": float, "user": "admin"}}
ACTIVE_SESSIONS: dict[str, dict] = {}
# Sessions are also persisted to CONFIG_PATH so a service restart does not
# log every client out (otherwise stats / OTA / photos / dev-studio all 401
# until a manual re-login).
_SESSION_LOCK = threading.Lock()
SESSION_TTL_SECS = 86400 * 7 # 7 days
def _load_sessions(force: bool = False) -> None:
"""Populate ACTIVE_SESSIONS from the persisted copy in CONFIG_PATH."""
if not force and ACTIVE_SESSIONS:
return
try:
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, "r") as f:
cfg = json.load(f)
saved = cfg.get("admin_sessions") or {}
now = time.time()
for tok, meta in saved.items():
if now - float(meta.get("created_at", 0)) < SESSION_TTL_SECS:
ACTIVE_SESSIONS[tok] = meta
except Exception:
logger.debug("Could not load persisted admin sessions", exc_info=True)
def _save_sessions() -> None:
"""Persist ACTIVE_SESSIONS into CONFIG_PATH (best-effort, atomic)."""
try:
with _SESSION_LOCK:
cfg = {}
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, "r") as f:
cfg = json.load(f)
cfg["admin_sessions"] = {
tok: m for tok, m in ACTIVE_SESSIONS.items()
}
tmp = CONFIG_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(cfg, f, indent=2)
os.replace(tmp, CONFIG_PATH)
except Exception:
logger.debug("Could not persist admin sessions", exc_info=True)
DASHBOARD_HTML = """
rndrSBC Management Portal
rS
rndrSBC Portal
E-Paper Management
🔒
Administrator sign-in required
This page is locked behind admin authentication.
🧪 Dev Studio
Preview any discovered widget against your real panel dimensions
🧪
Select a widget to render a preview.
🔎 Widget Finder
Every widget discovered on this install, with its configuration schema
Loading widget catalogue…
🔒
Welcome to rndrSBC
Please establish an administrator password to secure your display management portal.