diff --git a/frontend/js/api.js b/frontend/js/api.js index e7f9772..748648c 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -121,6 +121,9 @@ export const api = { // Device Groups - Playlist groupAssignPlaylist: (groupId, playlist_id) => request(`/groups/${groupId}/assign-playlist`, { method: 'POST', body: JSON.stringify({ playlist_id }) }), + // Current user + updateMe: (data) => request('/auth/me', { method: 'PUT', body: JSON.stringify(data) }), + // Admin - Users getUsers: () => request('/auth/users'), deleteUser: (id) => request(`/auth/users/${id}`, { method: 'DELETE' }), diff --git a/frontend/js/views/settings.js b/frontend/js/views/settings.js index 5a63fca..830e474 100644 --- a/frontend/js/views/settings.js +++ b/frontend/js/views/settings.js @@ -17,6 +17,30 @@ export async function render(container) { +
+

Account

+
+
+
+
+ + + ${user.auth_provider === 'local' ? ` +
+

Change Password

+

Must be at least 8 characters.

+
+
+
+
+
+ +
+ ` : ` +

You sign in via ${esc(user.auth_provider || 'SSO')}. Manage your password there.

+ `} +
+ ${isAdmin ? `

License

@@ -255,6 +279,45 @@ export async function render(container) { setLanguage(e.target.value); showToast('Language changed. Refresh for full effect.', 'info'); }); + + document.getElementById('saveAcctBtn')?.addEventListener('click', async () => { + const name = document.getElementById('acctName').value.trim(); + if (!name) return showToast('Name cannot be empty', 'error'); + const btn = document.getElementById('saveAcctBtn'); + btn.disabled = true; + try { + const updated = await api.updateMe({ name }); + const stored = JSON.parse(localStorage.getItem('user') || '{}'); + localStorage.setItem('user', JSON.stringify({ ...stored, ...updated })); + showToast('Profile saved', 'success'); + } catch (err) { + showToast(err.message, 'error'); + } finally { + btn.disabled = false; + } + }); + + document.getElementById('changePwBtn')?.addEventListener('click', async () => { + const current = document.getElementById('acctCurrentPw').value; + const next = document.getElementById('acctNewPw').value; + const confirm = document.getElementById('acctConfirmPw').value; + if (!current) return showToast('Enter your current password', 'error'); + if (next.length < 8) return showToast('New password must be at least 8 characters', 'error'); + if (next !== confirm) return showToast('New passwords do not match', 'error'); + const btn = document.getElementById('changePwBtn'); + btn.disabled = true; + try { + await api.updateMe({ current_password: current, password: next }); + document.getElementById('acctCurrentPw').value = ''; + document.getElementById('acctNewPw').value = ''; + document.getElementById('acctConfirmPw').value = ''; + showToast('Password changed', 'success'); + } catch (err) { + showToast(err.message, 'error'); + } finally { + btn.disabled = false; + } + }); } async function loadWhiteLabel() { diff --git a/server/routes/auth.js b/server/routes/auth.js index 9e1f610..cc54753 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -227,12 +227,23 @@ router.get('/me', requireAuth, (req, res) => { // Update current user router.put('/me', requireAuth, (req, res) => { - const { name, password } = req.body; + const { name, password, current_password } = req.body; if (name) { db.prepare('UPDATE users SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?') .run(name, req.user.id); } - if (password && password.length >= 8) { + if (password) { + if (password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' }); + const row = db.prepare('SELECT password_hash, auth_provider FROM users WHERE id = ?').get(req.user.id); + if (!row) return res.status(404).json({ error: 'User not found' }); + if (row.auth_provider !== 'local') { + return res.status(400).json({ error: `Your account signs in via ${row.auth_provider}. Manage your password there.` }); + } + if (row.password_hash) { + if (!current_password || !bcrypt.compareSync(current_password, row.password_hash)) { + return res.status(401).json({ error: 'Current password is incorrect' }); + } + } const hash = bcrypt.hashSync(password, 10); db.prepare('UPDATE users SET password_hash = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?') .run(hash, req.user.id);