/* * My Accounts — per-profile self-auth for playlist services. * * Each profile connects its OWN streaming accounts (their token, the app's * shared client). Used for that profile's playlist operations; the global/admin * auth keeps running the background app. Spotify is the first service; the others * follow the same pattern. * * Backend: GET /api/profiles/me/connections, the per-service OAuth popups, and * POST /api/profiles/me/connections//disconnect. */ // Playlist services shown in My Accounts. `connect` returns the OAuth URL for a // given profile id (popup); services are wired in over time. const _MA_SERVICES = [ { id: 'spotify', name: 'Spotify', brand: '#1db954', logo: 'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png', connect: (pid) => `/auth/spotify?profile_id=${pid}`, }, { id: 'tidal', name: 'Tidal', brand: '#00cfe8', logo: 'https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/tidal.png', connect: (pid) => `/auth/tidal?profile_id=${pid}`, }, { id: 'listenbrainz', name: 'ListenBrainz', brand: '#eb743b', dark: true, logo: 'https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/listenbrainz.png', type: 'token', saveUrl: '/api/profiles/me/listenbrainz', hint: 'Paste your token from listenbrainz.org/profile', }, ]; function _maEsc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function _maProfileId() { try { const ctx = (typeof getCurrentProfileContext === 'function') ? getCurrentProfileContext() : null; return ctx ? ctx.profileId : 1; } catch (_e) { return 1; } } function openMyAccountsModal() { let overlay = document.getElementById('my-accounts-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'my-accounts-overlay'; overlay.className = 'modal-overlay ma-overlay hidden'; overlay.onclick = (e) => { if (e.target === overlay) closeMyAccountsModal(); }; overlay.innerHTML = ` `; document.body.appendChild(overlay); } overlay.classList.remove('hidden'); const modal = overlay.querySelector('.ma-modal'); if (modal) { modal.classList.remove('ma-in'); void modal.offsetWidth; modal.classList.add('ma-in'); } document.addEventListener('keydown', _maOnKeydown); _maLoad(); } function closeMyAccountsModal() { const o = document.getElementById('my-accounts-overlay'); if (o) o.classList.add('hidden'); document.removeEventListener('keydown', _maOnKeydown); } function _maOnKeydown(e) { if (e.key === 'Escape') closeMyAccountsModal(); } async function _maLoad() { const body = document.getElementById('ma-body'); if (body) body.innerHTML = '
Loading…
'; let data = null; try { data = await (await fetch('/api/profiles/me/connections')).json(); } catch (e) { /* render disconnected */ } _maRender(body, data || { connections: {}, is_admin: false }); } function _maRender(body, data) { const conns = data.connections || {}; const isAdmin = !!data.is_admin; const rows = _MA_SERVICES.map(svc => { const c = conns[svc.id] || {}; const connected = !!c.connected; // Admin uses the global app account (set up in Settings) for every // service — not a personal connection here. const adminNote = isAdmin; let action; if (adminNote) { action = `Managed in Settings (app account)`; } else if (connected) { action = ` ${_maEsc(c.account || 'Connected')} `; } else if (svc.type === 'token') { action = ` `; } else { action = ``; } return `
${_maEsc(svc.name)}
${connected ? 'Connected' : (adminNote ? '' : 'Not connected')}
${action}
`; }).join(''); body.innerHTML = rows || '
No services available.
'; } let _maPollTimer = null; function connectMyAccount(serviceId) { const svc = _MA_SERVICES.find(s => s.id === serviceId); if (!svc) return; const pid = _maProfileId(); const popup = window.open(svc.connect(pid), 'soulsync-connect-' + serviceId, 'width=560,height=720,menubar=no,toolbar=no'); // Poll for the popup closing, then refresh status. if (_maPollTimer) clearInterval(_maPollTimer); _maPollTimer = setInterval(() => { if (!popup || popup.closed) { clearInterval(_maPollTimer); _maPollTimer = null; setTimeout(_maLoad, 600); // give the callback a moment to persist } }, 800); } async function saveMyAccountToken(serviceId) { const svc = _MA_SERVICES.find(s => s.id === serviceId); if (!svc || !svc.saveUrl) return; const input = document.getElementById(`ma-token-${serviceId}`); const token = (input && input.value || '').trim(); if (!token) { if (typeof showToast === 'function') showToast('Paste a token first', 'info'); return; } try { const res = await fetch(svc.saveUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }); const data = await res.json(); if (data.success) { if (typeof showToast === 'function') showToast(`${svc.name} connected`, 'success'); _maLoad(); } else if (typeof showToast === 'function') { showToast(data.error || 'Could not connect', 'error'); } } catch (e) { if (typeof showToast === 'function') showToast('Could not connect', 'error'); } } async function disconnectMyAccount(serviceId) { if (!confirm(`Disconnect your ${serviceId} account from this profile?`)) return; try { const res = await fetch(`/api/profiles/me/connections/${serviceId}/disconnect`, { method: 'POST' }); const data = await res.json(); if (data.success) { if (typeof showToast === 'function') showToast('Disconnected', 'success'); _maLoad(); } else if (typeof showToast === 'function') { showToast(data.error || 'Disconnect failed', 'error'); } } catch (e) { /* no-op */ } }