From 0957f83411a967743d5b51c17ff69d50de069847 Mon Sep 17 00:00:00 2001 From: Gronod Date: Fri, 15 May 2026 20:46:56 +0100 Subject: [PATCH] feat: admin users can view all downloads with user badges - Admin users (Emby IsAdministrator) see a 'Show all users' toggle - When toggled, all tagged downloads are shown regardless of user - Each download card shows the tagged user's name as a badge - Non-admin users see only their own downloads (unchanged behavior) - Backend accepts ?showAll=true query param (admin-only) --- public/app.js | 22 +++++++++++++++++++++- public/index.html | 6 ++++++ public/style.css | 36 ++++++++++++++++++++++++++++++++++++ server/routes/auth.js | 8 ++++++-- server/routes/dashboard.js | 33 ++++++++++++++++++++------------- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/public/app.js b/public/app.js index 5ec4dd6..c01cc9f 100644 --- a/public/app.js +++ b/public/app.js @@ -2,6 +2,8 @@ let currentUser = null; let downloads = []; let refreshInterval = null; let currentRefreshRate = 5000; // default 5 seconds +let isAdmin = false; +let showAll = false; // Apply saved theme immediately (before DOMContentLoaded to avoid flash) (function() { @@ -17,6 +19,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('login-form').addEventListener('submit', handleLogin); document.getElementById('logout-btn').addEventListener('click', handleLogout); document.getElementById('refresh-rate').addEventListener('change', handleRefreshRateChange); + document.getElementById('show-all-toggle').addEventListener('change', handleShowAllToggle); }); function initThemeSwitcher() { @@ -48,6 +51,11 @@ function handleRefreshRateChange(e) { startAutoRefresh(); } +function handleShowAllToggle(e) { + showAll = e.target.checked; + fetchUserDownloads(true); +} + function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); @@ -62,6 +70,7 @@ async function checkAuthentication() { if (data.authenticated) { currentUser = data.user; + isAdmin = !!data.user.isAdmin; showDashboard(); fetchUserDownloads(true); startAutoRefresh(); @@ -93,6 +102,7 @@ async function handleLogin(e) { if (data.success) { currentUser = data.user; + isAdmin = !!data.user.isAdmin; showDashboard(); fetchUserDownloads(true); startAutoRefresh(); @@ -129,6 +139,7 @@ function showDashboard() { document.getElementById('login-container').style.display = 'none'; document.getElementById('dashboard-container').style.display = 'block'; document.getElementById('currentUser').textContent = currentUser.name || '-'; + document.getElementById('admin-controls').style.display = isAdmin ? 'flex' : 'none'; } function showLoginError(message) { @@ -149,10 +160,12 @@ async function fetchUserDownloads(isInitialLoad = false) { hideError(); try { - const response = await fetch('/api/dashboard/user-downloads'); + const url = showAll ? '/api/dashboard/user-downloads?showAll=true' : '/api/dashboard/user-downloads'; + const response = await fetch(url); const data = await response.json(); currentUser = data.user; + isAdmin = !!data.isAdmin; downloads = data.downloads; // Debug: log first download to see what fields are present @@ -348,6 +361,13 @@ function createDownloadCard(download) { movie.textContent = `Movie: ${download.movieName}`; infoDiv.appendChild(movie); } + + if (showAll && download.userTag) { + const userBadge = document.createElement('span'); + userBadge.className = 'download-user-badge'; + userBadge.textContent = download.userTag; + header.appendChild(userBadge); + } const details = document.createElement('div'); details.className = 'download-details'; diff --git a/public/index.html b/public/index.html index 196e9a6..dbf5ed5 100644 --- a/public/index.html +++ b/public/index.html @@ -46,6 +46,12 @@ +
Current User: - diff --git a/public/style.css b/public/style.css index 0dfcf99..cd72a7b 100644 --- a/public/style.css +++ b/public/style.css @@ -577,6 +577,42 @@ body { background: var(--accent-hover); } +/* ===== Admin Controls ===== */ +.admin-controls { + display: flex; + align-items: center; +} + +.toggle-label { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: 0.8rem; + color: var(--text-secondary); + background: var(--surface-alt); + padding: 4px 12px; + border-radius: 14px; +} + +.toggle-label input[type="checkbox"] { + width: 14px; + height: 14px; + cursor: pointer; + accent-color: var(--accent); +} + +.download-user-badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 0.65rem; + font-weight: 600; + text-transform: capitalize; + background: var(--accent-light); + color: var(--accent); + margin-left: auto; +} + /* ===== Mobile ===== */ @media (max-width: 768px) { .app-header { diff --git a/server/routes/auth.js b/server/routes/auth.js index 5a0aeff..7d488bc 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -37,9 +37,11 @@ router.post('/login', async (req, res) => { console.log(`[Auth] Login successful for user: ${user.Name}`); // Set authentication cookie + const isAdmin = !!(user.Policy && user.Policy.IsAdministrator); res.cookie('emby_user', JSON.stringify({ id: user.Id, name: user.Name, + isAdmin: isAdmin, token: authData.AccessToken }), { httpOnly: true, @@ -50,7 +52,8 @@ router.post('/login', async (req, res) => { success: true, user: { id: user.Id, - name: user.Name + name: user.Name, + isAdmin: isAdmin } }); } catch (error) { @@ -76,7 +79,8 @@ router.get('/me', (req, res) => { authenticated: true, user: { id: user.id, - name: user.name + name: user.name, + isAdmin: !!user.isAdmin } }); } catch (error) { diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 104673f..db1671d 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -53,7 +53,9 @@ router.get('/user-downloads', async (req, res) => { const user = JSON.parse(userCookie); const username = user.name.toLowerCase(); - console.log(`[Dashboard] Fetching downloads for authenticated user: ${user.name} (${username})`); + const isAdmin = !!user.isAdmin; + const showAll = isAdmin && req.query.showAll === 'true'; + console.log(`[Dashboard] Fetching downloads for authenticated user: ${user.name} (${username}), isAdmin: ${isAdmin}, showAll: ${showAll}`); // Get all service instances const sabInstances = getSABnzbdInstances(); @@ -301,7 +303,7 @@ router.get('/user-downloads', async (req, res) => { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { userDownloads.push({ type: 'series', title: nzbName, @@ -314,7 +316,8 @@ router.get('/user-downloads', async (req, res) => { speed: slotState.speed, eta: slot.timeleft, seriesName: series.title, - episodeInfo: sonarrMatch + episodeInfo: sonarrMatch, + userTag: userTag }); } } @@ -330,7 +333,7 @@ router.get('/user-downloads', async (req, res) => { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { userDownloads.push({ type: 'movie', title: nzbName, @@ -343,7 +346,8 @@ router.get('/user-downloads', async (req, res) => { speed: slotState.speed, eta: slot.timeleft, movieName: movie.title, - movieInfo: radarrMatch + movieInfo: radarrMatch, + userTag: userTag }); } } @@ -376,7 +380,7 @@ router.get('/user-downloads', async (req, res) => { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { userDownloads.push({ type: 'series', title: nzbName, @@ -385,7 +389,8 @@ router.get('/user-downloads', async (req, res) => { size: slot.size, completedAt: slot.completed_time, seriesName: series.title, - episodeInfo: sonarrMatch + episodeInfo: sonarrMatch, + userTag: userTag }); } } @@ -401,7 +406,7 @@ router.get('/user-downloads', async (req, res) => { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { userDownloads.push({ type: 'movie', title: nzbName, @@ -410,7 +415,8 @@ router.get('/user-downloads', async (req, res) => { size: slot.size, completedAt: slot.completed_time, movieName: movie.title, - movieInfo: radarrMatch + movieInfo: radarrMatch, + userTag: userTag }); } } @@ -462,7 +468,7 @@ router.get('/user-downloads', async (req, res) => { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'series'; @@ -486,7 +492,7 @@ router.get('/user-downloads', async (req, res) => { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'movie'; @@ -510,7 +516,7 @@ router.get('/user-downloads', async (req, res) => { const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series; if (series) { const userTag = extractUserTag(series.tags, sonarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'series'; @@ -534,7 +540,7 @@ router.get('/user-downloads', async (req, res) => { const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie; if (movie) { const userTag = extractUserTag(movie.tags, radarrTagMap); - if (userTag && userTag.toLowerCase() === username) { + if (userTag && (showAll || userTag.toLowerCase() === username)) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'movie'; @@ -561,6 +567,7 @@ router.get('/user-downloads', async (req, res) => { res.json({ user: user.name, + isAdmin: isAdmin, downloads: userDownloads }); } catch (error) {