/* * Service credential sets — admin manager (Settings) + per-profile quick-switch * modal (sidebar Service Status). * * Admin creates named credential "pills" per auth service; every profile picks * which one is active for it. Secrets are entered here but NEVER read back — * the API only ever returns id/label, so this UI shows names, never values. * * Backend: /api/credentials (admin CRUD) and /api/profiles/me/services[/select] * (per-profile selection, any profile). */ // Display order + labels for the supported services (mirrors the backend // SERVICE_CREDENTIAL_SCHEMA). Each lists the fields the admin enters; `req` // marks required (matches server validation), `pw` renders as a password input. const CRED_SERVICES = [ { id: 'spotify', name: 'Spotify', fields: [ { key: 'client_id', label: 'Client ID', req: true }, { key: 'client_secret', label: 'Client Secret', req: true, pw: true }, { key: 'redirect_uri', label: 'Redirect URI (optional)' }, ]}, { id: 'tidal', name: 'Tidal', fields: [ { key: 'access_token', label: 'Access Token', req: true, pw: true }, { key: 'refresh_token', label: 'Refresh Token', req: true, pw: true }, ]}, { id: 'deezer', name: 'Deezer', fields: [ { key: 'arl', label: 'ARL', req: true, pw: true }, ]}, { id: 'qobuz', name: 'Qobuz', fields: [ { key: 'user_auth_token', label: 'User Auth Token', req: true, pw: true }, ]}, { id: 'plex', name: 'Plex', fields: [ { key: 'base_url', label: 'Server URL', req: true }, { key: 'token', label: 'Token', req: true, pw: true }, ]}, { id: 'jellyfin', name: 'Jellyfin', fields: [ { key: 'base_url', label: 'Server URL', req: true }, { key: 'api_key', label: 'API Key', req: true, pw: true }, ]}, { id: 'navidrome', name: 'Navidrome', fields: [ { key: 'base_url', label: 'Server URL', req: true }, { key: 'username', label: 'Username', req: true }, { key: 'password', label: 'Password', req: true, pw: true }, ]}, ]; const _credServiceById = Object.fromEntries(CRED_SERVICES.map(s => [s.id, s])); function _credEsc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // ── Admin manager (rendered inside Settings) ───────────────────────────────── async function loadCredentialSets() { const host = document.getElementById('credential-sets-container'); if (!host) return; try { const res = await fetch('/api/credentials'); if (res.status === 403) { host.innerHTML = ''; return; } // not admin const data = await res.json(); _renderCredentialSets(host, (data && data.services) || {}); } catch (e) { host.innerHTML = '