a simple web player for subsonic
tinysub.devins.page
subsonic
navidrome
javascript
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}