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);