From 4d1bb78c834ac78322b7c7904a6b3787d9d1c73c Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 23 Mar 2026 13:59:55 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20make=20authentication=20optional=20?= =?UTF-8?q?=E2=80=94=20no=20tokens=20=3D=20no=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no api_tokens are configured (the new default), all endpoints are accessible without authentication. The frontend detects this via /api/health's auth_required field and skips the login form. - Backend: auth.py skips verification when api_tokens is empty - Frontend: shared getAuthHeaders()/hasCredentials() helpers replace scattered token logic across all JS modules - Health endpoint exposes auth_required for frontend discovery - config.example.yaml ships with tokens commented out - CLI --show-token and startup log reflect disabled state Co-Authored-By: Claude Opus 4.6 (1M context) --- config.example.yaml | 11 +++--- media_server/auth.py | 22 ++++++++++-- media_server/config.py | 13 ++++--- media_server/main.py | 54 +++++++++++++++++------------ media_server/routes/health.py | 3 ++ media_server/routes/media.py | 17 ++++----- media_server/static/js/app.js | 20 ++++++++++- media_server/static/js/browser.js | 49 +++++++------------------- media_server/static/js/callbacks.js | 20 +++-------- media_server/static/js/core.js | 35 ++++++++++++++----- media_server/static/js/links.js | 42 ++++++---------------- media_server/static/js/player.js | 19 ++++------ media_server/static/js/scripts.js | 48 ++++++------------------- media_server/static/js/websocket.js | 12 ++++--- 14 files changed, 175 insertions(+), 190 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 83b722d..a699974 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,13 +1,14 @@ # Media Server Configuration # Copy this file to config.yaml and customize as needed. -# A secure token will be auto-generated on first run if not specified. +# By default, authentication is DISABLED (no tokens = open access). +# To enable auth, uncomment and configure the api_tokens section below. # API Tokens - Multiple tokens with friendly labels # This allows you to identify which client is making requests in the logs -api_tokens: - home_assistant: "your-home-assistant-token-here" - mobile: "your-mobile-app-token-here" - web_ui: "your-web-ui-token-here" +# api_tokens: +# home_assistant: "your-home-assistant-token-here" +# mobile: "your-mobile-app-token-here" +# web_ui: "your-web-ui-token-here" # Server settings host: "0.0.0.0" diff --git a/media_server/auth.py b/media_server/auth.py index 0a42d23..6a319a8 100644 --- a/media_server/auth.py +++ b/media_server/auth.py @@ -15,6 +15,11 @@ security = HTTPBearer(auto_error=False) token_label_var: ContextVar[str] = ContextVar("token_label", default="unknown") +def auth_enabled() -> bool: + """Check if authentication is enabled (i.e. at least one token is configured).""" + return bool(settings.api_tokens) + + def get_token_label(token: str) -> Optional[str]: """Get the label for a token. Returns None if token is invalid. @@ -36,14 +41,19 @@ async def verify_token( ) -> str: """Verify the API token from the Authorization header. + When no tokens are configured, authentication is skipped entirely. Reuses the label from middleware context when already validated. Returns: - The token label + The token label (or "anonymous" when auth is disabled) Raises: - HTTPException: If the token is missing or invalid + HTTPException: If the token is missing or invalid (only when auth enabled) """ + if not auth_enabled(): + token_label_var.set("anonymous") + return "anonymous" + # Reuse label already set by middleware to avoid redundant O(n) scan existing = token_label_var.get("unknown") if existing != "unknown": @@ -80,6 +90,10 @@ class TokenAuth: credentials: HTTPAuthorizationCredentials = Depends(security), ) -> str | None: """Verify the token and return the label or raise an exception.""" + if not auth_enabled(): + token_label_var.set("anonymous") + return "anonymous" + if credentials is None: if self.auto_error: raise HTTPException( @@ -122,6 +136,10 @@ async def verify_token_or_query( Raises: HTTPException: If the token is missing or invalid """ + if not auth_enabled(): + token_label_var.set("anonymous") + return "anonymous" + # Reuse label already set by middleware existing = token_label_var.get("unknown") if existing != "unknown": diff --git a/media_server/config.py b/media_server/config.py index 00f1faf..9dcd574 100644 --- a/media_server/config.py +++ b/media_server/config.py @@ -1,7 +1,6 @@ """Configuration management for the media server.""" import os -import secrets from pathlib import Path from typing import Optional @@ -62,10 +61,10 @@ class Settings(BaseSettings): host: str = Field(default="0.0.0.0", description="Server bind address") port: int = Field(default=8765, description="Server port") - # Authentication + # Authentication (empty = auth disabled, anyone can access the API) api_tokens: dict[str, str] = Field( - default_factory=lambda: {"default": secrets.token_urlsafe(32)}, - description="Named API tokens for access control (label: token pairs)", + default_factory=dict, + description="Named API tokens for access control (label: token pairs). Empty = no auth.", ) # Media controller settings @@ -188,9 +187,9 @@ def generate_default_config(path: Optional[Path] = None) -> Path: config = { "host": "0.0.0.0", "port": 8765, - "api_tokens": { - "default": secrets.token_urlsafe(32), - }, + # "api_tokens": { + # "default": "your-secret-token-here", + # }, "poll_interval": 1.0, "log_level": "INFO", # Audio device to control (use GET /api/audio/devices to list available devices) diff --git a/media_server/main.py b/media_server/main.py index 2a45643..38458d5 100644 --- a/media_server/main.py +++ b/media_server/main.py @@ -59,9 +59,12 @@ async def lifespan(app: FastAPI): logger = logging.getLogger(__name__) logger.info(f"Media Server starting on {settings.host}:{settings.port}") - # Log all configured tokens - for label, token in settings.api_tokens.items(): - logger.info(f"API Token [{label}]: {token[:8]}...") + # Log authentication status + if settings.api_tokens: + for label, token in settings.api_tokens.items(): + logger.info(f"API Token [{label}]: {token[:8]}...") + else: + logger.warning("No API tokens configured — authentication is DISABLED") # Start WebSocket status monitor controller = get_media_controller() @@ -129,24 +132,28 @@ def create_app() -> FastAPI: @app.middleware("http") async def token_logging_middleware(request: Request, call_next): """Extract token label and set in context for logging.""" - token_label = "unknown" + if not settings.api_tokens: + token_label_var.set("anonymous") + else: + token_label = "unknown" - # Try Authorization header - auth_header = request.headers.get("authorization", "") - if auth_header.startswith("Bearer "): - token = auth_header[7:] - label = get_token_label(token) - if label: - token_label = label + # Try Authorization header + auth_header = request.headers.get("authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header[7:] + label = get_token_label(token) + if label: + token_label = label - # Try query parameter (for artwork endpoint) - elif "token" in request.query_params: - token = request.query_params["token"] - label = get_token_label(token) - if label: - token_label = label + # Try query parameter (for artwork endpoint) + elif "token" in request.query_params: + token = request.query_params["token"] + label = get_token_label(token) + if label: + token_label = label + + token_label_var.set(token_label) - token_label_var.set(token_label) response = await call_next(request) return response @@ -215,14 +222,17 @@ def main(): if args.generate_config: config_path = generate_default_config() print(f"Configuration file generated at: {config_path}") - print("API Token has been saved to the config file.") + print("Authentication is disabled by default. Add api_tokens to enable it.") return if args.show_token: print(f"Config directory: {get_config_dir()}") - print("\nAPI Tokens:") - for label, token in settings.api_tokens.items(): - print(f" {label:20} {token}") + if settings.api_tokens: + print("\nAPI Tokens:") + for label, token in settings.api_tokens.items(): + print(f" {label:20} {token}") + else: + print("\nAuthentication is DISABLED (no tokens configured)") return uvicorn.run( diff --git a/media_server/routes/health.py b/media_server/routes/health.py index 092e8b0..f61c2db 100644 --- a/media_server/routes/health.py +++ b/media_server/routes/health.py @@ -5,6 +5,8 @@ from typing import Any from fastapi import APIRouter +from ..auth import auth_enabled + router = APIRouter(prefix="/api", tags=["health"]) @@ -19,4 +21,5 @@ async def health_check() -> dict[str, Any]: "status": "healthy", "platform": platform.system(), "version": "1.0.0", + "auth_required": auth_enabled(), } diff --git a/media_server/routes/media.py b/media_server/routes/media.py index eee667f..ab69804 100644 --- a/media_server/routes/media.py +++ b/media_server/routes/media.py @@ -334,15 +334,16 @@ async def websocket_endpoint( - {"type": "get_status"} - Request current status """ # Verify token - from ..auth import get_token_label, token_label_var + from ..auth import auth_enabled, get_token_label, token_label_var - label = get_token_label(token) if token else None - if label is None: - await websocket.close(code=4001, reason="Invalid authentication token") - return - - # Set label in context for logging - token_label_var.set(label) + if auth_enabled(): + label = get_token_label(token) if token else None + if label is None: + await websocket.close(code=4001, reason="Invalid authentication token") + return + token_label_var.set(label) + else: + token_label_var.set("anonymous") await ws_manager.connect(websocket) diff --git a/media_server/static/js/app.js b/media_server/static/js/app.js index 7011281..3173fc4 100644 --- a/media_server/static/js/app.js +++ b/media_server/static/js/app.js @@ -13,6 +13,7 @@ import { togglePlayPause, nextTrack, previousTrack, toggleMute, VOLUME_THROTTLE_MS, VOLUME_RELEASE_DELAY_MS, changeLocale, t, + setAuthRequired, } from './core.js'; // Layer 1: Player (tabs, theme, accent, vinyl, visualizer, UI) @@ -160,8 +161,25 @@ window.addEventListener('DOMContentLoaded', async () => { // Load version from health endpoint fetchVersion(); + // Check if authentication is required + let authReq = true; + try { + const healthResp = await fetch('/api/health'); + const healthData = await healthResp.json(); + authReq = healthData.auth_required !== false; + } catch { /* assume auth required on error */ } + setAuthRequired(authReq); + const token = localStorage.getItem('media_server_token'); - if (token) { + if (!authReq) { + // No auth required — connect directly without token + connectWebSocket(''); + loadScripts(); + loadScriptsTable(); + loadCallbacksTable(); + loadLinksTable(); + loadAudioDevices(); + } else if (token) { connectWebSocket(token); loadScripts(); loadScriptsTable(); diff --git a/media_server/static/js/browser.js b/media_server/static/js/browser.js index f1a7bfd..d115a3d 100644 --- a/media_server/static/js/browser.js +++ b/media_server/static/js/browser.js @@ -5,6 +5,7 @@ import { t, showToast, escapeHtml, closeDialog, SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml, + getAuthHeaders, hasCredentials, } from './core.js'; // Browser state @@ -24,14 +25,10 @@ const THUMBNAIL_CACHE_MAX = 200; // Load media folders on page load export async function loadMediaFolders() { try { - const token = localStorage.getItem('media_server_token'); - if (!token) { - console.error('No API token found'); - return; - } + if (!hasCredentials()) return; const response = await fetch('/api/browser/folders', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) throw new Error('Failed to load folders'); @@ -119,11 +116,7 @@ async function browsePath(folderId, path, offset = 0, nocache = false) { showBrowserSearch(false); try { - const token = localStorage.getItem('media_server_token'); - if (!token) { - console.error('No API token found'); - return; - } + if (!hasCredentials()) return; // Show loading spinner const container = document.getElementById('browserGrid'); @@ -135,7 +128,7 @@ async function browsePath(folderId, path, offset = 0, nocache = false) { if (nocache) url += '&nocache=true'; const response = await fetch( url, - { headers: { 'Authorization': `Bearer ${token}` } } + { headers: getAuthHeaders() } ); if (!response.ok) { @@ -487,11 +480,7 @@ function formatBitrate(bps) { async function loadThumbnail(imgElement, fileName) { try { - const token = localStorage.getItem('media_server_token'); - if (!token) { - console.error('No API token found'); - return; - } + if (!hasCredentials()) return; const absolutePath = buildAbsolutePath(currentFolderId, currentPath, fileName); @@ -510,7 +499,7 @@ async function loadThumbnail(imgElement, fileName) { const response = await fetch( `/api/browser/thumbnail?path=${encodedPath}&size=medium`, - { headers: { 'Authorization': `Bearer ${token}` } } + { headers: getAuthHeaders() } ); if (response.status === 200) { @@ -576,20 +565,13 @@ async function playMediaFile(fileName) { if (playInProgress) return; playInProgress = true; try { - const token = localStorage.getItem('media_server_token'); - if (!token) { - console.error('No API token found'); - return; - } + if (!hasCredentials()) return; const absolutePath = buildAbsolutePath(currentFolderId, currentPath, fileName); const response = await fetch('/api/browser/play', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ path: absolutePath }) }); @@ -610,15 +592,11 @@ export async function playAllFolder() { const btn = document.getElementById('playAllBtn'); if (btn) btn.disabled = true; try { - const token = localStorage.getItem('media_server_token'); - if (!token || !currentFolderId) return; + if (!hasCredentials() || !currentFolderId) return; const response = await fetch('/api/browser/play-folder', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ folder_id: currentFolderId, path: currentPath }) }); @@ -640,8 +618,7 @@ export async function playAllFolder() { export async function downloadFile(fileName, event) { if (event) event.stopPropagation(); - const token = localStorage.getItem('media_server_token'); - if (!token) return; + if (!hasCredentials()) return; const fullPath = currentPath === '/' ? '/' + fileName @@ -651,7 +628,7 @@ export async function downloadFile(fileName, event) { try { const response = await fetch( `/api/browser/download?folder_id=${currentFolderId}&path=${encodedPath}`, - { headers: { 'Authorization': `Bearer ${token}` } } + { headers: getAuthHeaders() } ); if (!response.ok) throw new Error('Download failed'); diff --git a/media_server/static/js/callbacks.js b/media_server/static/js/callbacks.js index f481c9f..e929d22 100644 --- a/media_server/static/js/callbacks.js +++ b/media_server/static/js/callbacks.js @@ -2,7 +2,7 @@ // Callbacks: CRUD management // ============================================================ -import { t, showToast, escapeHtml, closeDialog, showConfirm } from './core.js'; +import { t, showToast, escapeHtml, closeDialog, showConfirm, getAuthHeaders, hasCredentials } from './core.js'; export let callbackFormDirty = false; export function setCallbackFormDirty(value) { callbackFormDirty = value; } @@ -16,12 +16,11 @@ export async function loadCallbacksTable() { } async function _loadCallbacksTableImpl() { - const token = localStorage.getItem('media_server_token'); const tbody = document.getElementById('callbacksTableBody'); try { const response = await fetch('/api/callbacks/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -79,13 +78,12 @@ export function showAddCallbackDialog() { } export async function showEditCallbackDialog(callbackName) { - const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('callbackDialog'); const title = document.getElementById('callbackDialogTitle'); try { const response = await fetch('/api/callbacks/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -137,7 +135,6 @@ export async function saveCallback(event) { const submitBtn = event.target.querySelector('button[type="submit"]'); if (submitBtn) submitBtn.disabled = true; - const token = localStorage.getItem('media_server_token'); const isEdit = document.getElementById('callbackIsEdit').value === 'true'; const callbackName = document.getElementById('callbackName').value; @@ -157,10 +154,7 @@ export async function saveCallback(event) { try { const response = await fetch(endpoint, { method, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify(data) }); @@ -187,14 +181,10 @@ export async function deleteCallbackConfirm(callbackName) { return; } - const token = localStorage.getItem('media_server_token'); - try { const response = await fetch(`/api/callbacks/delete/${callbackName}`, { method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } + headers: getAuthHeaders() }); const result = await response.json(); diff --git a/media_server/static/js/core.js b/media_server/static/js/core.js index 8bdf964..c941661 100644 --- a/media_server/static/js/core.js +++ b/media_server/static/js/core.js @@ -300,8 +300,7 @@ function updateAllText() { document.getElementById('sourceIcon').innerHTML = initSrc?.icon || ''; } - const token = localStorage.getItem('media_server_token'); - if (token) { + if (hasCredentials()) { if (_loadScriptsTable) _loadScriptsTable(); if (_loadCallbacksTable) _loadCallbacksTable(); if (_loadLinksTable) _loadLinksTable(); @@ -396,19 +395,39 @@ export function showConfirm(message) { }); } +// ============================================================ +// Auth Helpers +// ============================================================ + +// Set to false when server reports auth_required: false +export let authRequired = true; +export function setAuthRequired(value) { authRequired = value; } + +/** + * Build Authorization headers for API requests. + * Returns empty object when auth is disabled or no token is stored. + */ +export function getAuthHeaders() { + const token = localStorage.getItem('media_server_token'); + return token ? { 'Authorization': `Bearer ${token}` } : {}; +} + +/** + * Check if we have sufficient credentials to call the API. + * True when auth is disabled OR a token is stored. + */ +export function hasCredentials() { + return !authRequired || !!localStorage.getItem('media_server_token'); +} + // ============================================================ // API Commands // ============================================================ export async function sendCommand(endpoint, body = null) { - const token = localStorage.getItem('media_server_token'); - const options = { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, }; if (body) { diff --git a/media_server/static/js/links.js b/media_server/static/js/links.js index a4abd8a..e2a9715 100644 --- a/media_server/static/js/links.js +++ b/media_server/static/js/links.js @@ -2,21 +2,20 @@ // Display Brightness & Power Control + Links Management // ============================================================ -import { t, showToast, escapeHtml, closeDialog, showConfirm, resolveMdiIcons, fetchMdiIcon } from './core.js'; +import { t, showToast, escapeHtml, closeDialog, showConfirm, resolveMdiIcons, fetchMdiIcon, getAuthHeaders, hasCredentials } from './core.js'; let displayBrightnessTimers = {}; const DISPLAY_THROTTLE_MS = 50; export async function loadDisplayMonitors() { - const token = localStorage.getItem('media_server_token'); - if (!token) return; + if (!hasCredentials()) return; const container = document.getElementById('displayMonitors'); if (!container) return; try { const response = await fetch('/api/display/monitors?refresh=true', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -108,14 +107,10 @@ export function onDisplayBrightnessChange(monitorId, value) { } async function sendDisplayBrightness(monitorId, brightness) { - const token = localStorage.getItem('media_server_token'); try { await fetch(`/api/display/brightness/${monitorId}`, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ brightness }) }); } catch (e) { @@ -128,14 +123,10 @@ export async function toggleDisplayPower(monitorId, monitorName) { const isOn = btn && btn.classList.contains('on'); const newState = !isOn; - const token = localStorage.getItem('media_server_token'); try { const response = await fetch(`/api/display/power/${monitorId}`, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ on: newState }) }); const data = await response.json(); @@ -160,15 +151,14 @@ export async function toggleDisplayPower(monitorId, monitorName) { // ============================================================ export async function loadHeaderLinks() { - const token = localStorage.getItem('media_server_token'); - if (!token) return; + if (!hasCredentials()) return; const container = document.getElementById('headerLinks'); if (!container) return; try { const response = await fetch('/api/links/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) return; @@ -210,12 +200,11 @@ export async function loadLinksTable() { } async function _loadLinksTableImpl() { - const token = localStorage.getItem('media_server_token'); const tbody = document.getElementById('linksTableBody'); try { const response = await fetch('/api/links/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -273,13 +262,12 @@ export function showAddLinkDialog() { } export async function showEditLinkDialog(linkName) { - const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('linkDialog'); const title = document.getElementById('linkDialogTitle'); try { const response = await fetch('/api/links/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -342,7 +330,6 @@ export async function saveLink(event) { const submitBtn = event.target.querySelector('button[type="submit"]'); if (submitBtn) submitBtn.disabled = true; - const token = localStorage.getItem('media_server_token'); const isEdit = document.getElementById('linkIsEdit').value === 'true'; const linkName = isEdit ? document.getElementById('linkOriginalName').value : @@ -364,10 +351,7 @@ export async function saveLink(event) { try { const response = await fetch(endpoint, { method, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify(data) }); @@ -393,14 +377,10 @@ export async function deleteLinkConfirm(linkName) { return; } - const token = localStorage.getItem('media_server_token'); - try { const response = await fetch(`/api/links/delete/${linkName}`, { method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } + headers: getAuthHeaders() }); const result = await response.json(); diff --git a/media_server/static/js/player.js b/media_server/static/js/player.js index 46e2bf1..956eebf 100644 --- a/media_server/static/js/player.js +++ b/media_server/static/js/player.js @@ -9,6 +9,7 @@ import { currentPosition, setCurrentPosition, isUserAdjustingVolume, lastStatus, setLastStatus, currentPlayState, setCurrentPlayState, POSITION_INTERPOLATION_MS, seek, + getAuthHeaders, hasCredentials, } from './core.js'; import { updateBackgroundColors } from './background.js'; import { loadDisplayMonitors } from './links.js'; @@ -282,9 +283,8 @@ const VISUALIZER_SMOOTHING = 0.15; export async function checkVisualizerAvailability() { try { - const token = localStorage.getItem('media_server_token'); const resp = await fetch('/api/media/visualizer/status', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (resp.ok) { const data = await resp.json(); @@ -428,14 +428,12 @@ export async function loadAudioDevices() { if (!section || !select) return; try { - const token = localStorage.getItem('media_server_token'); - const [devicesResp, statusResp] = await Promise.all([ fetch('/api/media/visualizer/devices', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }), fetch('/api/media/visualizer/status', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }) ]); @@ -495,15 +493,11 @@ export async function onAudioDeviceChanged() { if (!select) return; const deviceName = select.value || null; - const token = localStorage.getItem('media_server_token'); try { const resp = await fetch('/api/media/visualizer/device', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ device_name: deviceName }) }); @@ -612,9 +606,8 @@ export function updateUI(status) { const placeholderArt = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E"; const placeholderGlow = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E"; if (artworkSource) { - const token = localStorage.getItem('media_server_token'); fetch(`/api/media/artwork?_=${Date.now()}`, { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }) .then(r => r.ok ? r.blob() : null) .then(blob => { diff --git a/media_server/static/js/scripts.js b/media_server/static/js/scripts.js index bae5647..54a28a6 100644 --- a/media_server/static/js/scripts.js +++ b/media_server/static/js/scripts.js @@ -6,19 +6,16 @@ import { t, showToast, escapeHtml, closeDialog, showConfirm, resolveMdiIcons, fetchMdiIcon, scripts, setScripts, + getAuthHeaders, hasCredentials, } from './core.js'; export let scriptFormDirty = false; export function setScriptFormDirty(value) { scriptFormDirty = value; } export async function loadScripts() { - const token = localStorage.getItem('media_server_token'); - try { const response = await fetch('/api/scripts/list', { - headers: { - 'Authorization': `Bearer ${token}` - } + headers: getAuthHeaders() }); if (response.ok) { @@ -67,10 +64,9 @@ export async function displayQuickAccess() { }); try { - const token = localStorage.getItem('media_server_token'); - if (token) { + if (hasCredentials()) { const response = await fetch('/api/links/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (gen !== _quickAccessGen) return; if (response.ok) { @@ -124,16 +120,12 @@ export async function displayQuickAccess() { } async function executeScript(scriptName, buttonElement) { - const token = localStorage.getItem('media_server_token'); buttonElement.classList.add('executing'); try { const response = await fetch(`/api/scripts/execute/${scriptName}`, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ args: [] }) }); @@ -165,12 +157,11 @@ export async function loadScriptsTable() { } async function _loadScriptsTableImpl() { - const token = localStorage.getItem('media_server_token'); const tbody = document.getElementById('scriptsTableBody'); try { const response = await fetch('/api/scripts/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -232,13 +223,12 @@ export function showAddScriptDialog() { } export async function showEditScriptDialog(scriptName) { - const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('scriptDialog'); const title = document.getElementById('dialogTitle'); try { const response = await fetch('/api/scripts/list', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) { @@ -300,7 +290,6 @@ export async function saveScript(event) { const submitBtn = event.target.querySelector('button[type="submit"]'); if (submitBtn) submitBtn.disabled = true; - const token = localStorage.getItem('media_server_token'); const isEdit = document.getElementById('scriptIsEdit').value === 'true'; const scriptName = isEdit ? document.getElementById('scriptOriginalName').value : @@ -324,10 +313,7 @@ export async function saveScript(event) { try { const response = await fetch(endpoint, { method, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify(data) }); @@ -353,14 +339,10 @@ export async function deleteScriptConfirm(scriptName) { return; } - const token = localStorage.getItem('media_server_token'); - try { const response = await fetch(`/api/scripts/delete/${scriptName}`, { method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } + headers: getAuthHeaders() }); const result = await response.json(); @@ -443,7 +425,6 @@ function showExecutionResult(name, result, type = 'script') { } export async function executeScriptDebug(scriptName) { - const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('executionDialog'); const title = document.getElementById('executionDialogTitle'); const statusDiv = document.getElementById('executionStatus'); @@ -463,10 +444,7 @@ export async function executeScriptDebug(scriptName) { try { const response = await fetch(`/api/scripts/execute/${scriptName}`, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ args: [] }) }); @@ -494,7 +472,6 @@ export async function executeScriptDebug(scriptName) { } export async function executeCallbackDebug(callbackName) { - const token = localStorage.getItem('media_server_token'); const dialog = document.getElementById('executionDialog'); const title = document.getElementById('executionDialogTitle'); const statusDiv = document.getElementById('executionStatus'); @@ -514,10 +491,7 @@ export async function executeCallbackDebug(callbackName) { try { const response = await fetch(`/api/callbacks/execute/${callbackName}`, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() } }); const result = await response.json(); diff --git a/media_server/static/js/websocket.js b/media_server/static/js/websocket.js index cafe82e..ac061ed 100644 --- a/media_server/static/js/websocket.js +++ b/media_server/static/js/websocket.js @@ -6,6 +6,7 @@ import { dom, t, showToast, setWs, WS_BACKOFF_BASE_MS, WS_BACKOFF_MAX_MS, WS_MAX_RECONNECT_ATTEMPTS, WS_PING_INTERVAL_MS, + authRequired, } from './core.js'; import { updateUI, visualizerEnabled, visualizerAvailable, setFrequencyData, stopPositionInterpolation, loadAudioDevices } from './player.js'; import { loadScripts, loadScriptsTable, displayQuickAccess } from './scripts.js'; @@ -62,7 +63,8 @@ export function connectWebSocket(token) { } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/api/media/ws?token=${encodeURIComponent(token)}`; + const wsBase = `${protocol}//${window.location.host}/api/media/ws`; + const wsUrl = token ? `${wsBase}?token=${encodeURIComponent(token)}` : wsBase; const newWs = new WebSocket(wsUrl); setWs(newWs); @@ -134,8 +136,8 @@ export function connectWebSocket(token) { reconnectTimeout = setTimeout(() => { const savedToken = localStorage.getItem('media_server_token'); - if (savedToken) { - connectWebSocket(savedToken); + if (savedToken || !authRequired) { + connectWebSocket(savedToken || ''); } }, delay); } else { @@ -175,9 +177,9 @@ function hideConnectionBanner() { export function manualReconnect() { const savedToken = localStorage.getItem('media_server_token'); - if (savedToken) { + if (savedToken || !authRequired) { wsReconnectAttempts = 0; hideConnectionBanner(); - connectWebSocket(savedToken); + connectWebSocket(savedToken || ''); } }