a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
at main 170 lines 4.4 kB view raw
1// authorization 2 3const CredentialManager = { 4 save: (server, username, token, salt) => { 5 const creds = { server, username, token, salt }; 6 localStorage.setItem("tinysub_credentials", JSON.stringify(creds)); 7 }, 8 load: () => { 9 const saved = localStorage.getItem("tinysub_credentials"); 10 return saved 11 ? JSON.parse(saved) 12 : { server: "", username: "", token: "", salt: "" }; 13 }, 14 clear: () => { 15 localStorage.removeItem("tinysub_credentials"); 16 }, 17}; 18 19// toggle auth modal 20const toggleAuthModal = (show) => { 21 const authModal = document.getElementById(DOM_IDS.AUTH_MODAL); 22 if (!authModal) return; 23 24 if (show) { 25 showModal(authModal, { 26 focusSelector: "input", 27 }); 28 } else { 29 hideModal(DOM_IDS.AUTH_MODAL); 30 } 31}; 32 33// load library, playlists, and favorites after successful login 34async function initializeApp() { 35 toggleAuthModal(false); 36 await loadQueue().catch(() => {}); 37 updateQueueDisplay(); 38 await loadLibrary(); 39 await loadPlaylists(); 40 await loadFavorites(); 41 // restore song display and pause playback on startup 42 if (hasValidTrack()) { 43 playTrack(state.queue[state.queueIndex]); 44 ui.player.pause(); 45 } 46} 47 48// handle login form submission with validation 49async function handleLogin() { 50 if (handleLogin.isInProgress) return; 51 52 const server = ui.serverInput.value; 53 const username = ui.usernameInput.value; 54 const password = ui.passwordInput.value; 55 56 ui.authError.textContent = ""; 57 58 const urlValidation = validateServerUrl(server); 59 if (!urlValidation.valid) { 60 ui.authError.textContent = urlValidation.error; 61 return; 62 } 63 64 const credValidation = validateCredentials(username, password); 65 if (!credValidation.valid) { 66 ui.authError.textContent = credValidation.error; 67 return; 68 } 69 70 handleLogin.isInProgress = true; 71 const { username: validUsername, password: validPassword } = 72 credValidation.value; 73 const validServerUrl = urlValidation.value; 74 75 try { 76 // Generate salt+token from password 77 const salt = Math.random().toString(36).substring(2, 8); 78 const token = SparkMD5.hash(validPassword + salt); 79 80 // Test with token mode API 81 state.api = new SubsonicAPI(validServerUrl, validUsername, token, salt); 82 await state.api.ping(); 83 84 // Save credentials for auto-login 85 CredentialManager.save(validServerUrl, validUsername, token, salt); 86 87 await initializeApp(); 88 } catch (error) { 89 console.error("[Auth] Login failed:", error); 90 ui.authError.textContent = `${STRINGS.CONNECTION_ERROR} ${error.message}`; 91 state.api = null; 92 } finally { 93 handleLogin.isInProgress = false; 94 } 95} 96 97// clear all storage and reload to logout 98async function handleLogout() { 99 try { 100 if (indexedDB.databases) { 101 const dbs = await indexedDB.databases(); 102 for (const db of dbs) { 103 indexedDB.deleteDatabase(db.name); 104 } 105 } 106 CredentialManager.clear(); 107 } catch (error) { 108 console.error("[Auth] Logout cleanup failed:", error); 109 } 110 location.reload(); 111} 112 113// attempt auto-login on page load 114async function attemptAutoLogin() { 115 const creds = CredentialManager.load(); 116 117 // security migration in case you were using version <1.8 which stored raw passwords 118 const oldCredentials = localStorage.getItem("tinysub_credentials"); 119 if (oldCredentials) { 120 try { 121 const parsed = JSON.parse(oldCredentials); 122 if (parsed.password) { 123 console.warn( 124 "[Auth] Old password format detected, clearing storage and logging out for security", 125 ); 126 await handleLogout(); 127 return; 128 } 129 } catch { 130 // ignore 131 } 132 } 133 134 // no stored credentials 135 if (!creds.server || !creds.username || !creds.token || !creds.salt) { 136 toggleAuthModal(true); 137 return; 138 } 139 140 try { 141 // try to authenticate with stored salt+token 142 state.api = new SubsonicAPI( 143 creds.server, 144 creds.username, 145 creds.token, 146 creds.salt, 147 ); 148 // validate credentials are still valid by calling ping() 149 await state.api.ping(); 150 await initializeApp(); 151 } catch (error) { 152 console.warn( 153 "[Auth] Stored credentials invalid, showing login form:", 154 error.message, 155 ); 156 state.api = null; 157 // pre-populate form with stored server and username for convenience 158 ui.serverInput.value = creds.server; 159 ui.usernameInput.value = creds.username; 160 ui.passwordInput.value = ""; // never prefill password 161 toggleAuthModal(true); 162 } 163} 164 165// run auto-login when scripts are ready 166if (document.readyState === "loading") { 167 document.addEventListener("DOMContentLoaded", attemptAutoLogin); 168} else { 169 attemptAutoLogin(); 170}