mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Settings: add account profile + password change UI
Adds a per-user Account section in Settings with name edit and password change. Password change requires current password; local auth only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
772ead28a2
commit
52297ec618
|
|
@ -121,6 +121,9 @@ export const api = {
|
||||||
// Device Groups - Playlist
|
// Device Groups - Playlist
|
||||||
groupAssignPlaylist: (groupId, playlist_id) => request(`/groups/${groupId}/assign-playlist`, { method: 'POST', body: JSON.stringify({ playlist_id }) }),
|
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
|
// Admin - Users
|
||||||
getUsers: () => request('/auth/users'),
|
getUsers: () => request('/auth/users'),
|
||||||
deleteUser: (id) => request(`/auth/users/${id}`, { method: 'DELETE' }),
|
deleteUser: (id) => request(`/auth/users/${id}`, { method: 'DELETE' }),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,30 @@ export async function render(container) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Account</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">
|
||||||
|
<div class="form-group"><label>Email</label><input type="email" class="input" value="${esc(user.email || '')}" disabled></div>
|
||||||
|
<div class="form-group"><label>Name</label><input type="text" id="acctName" class="input" value="${esc(user.name || '')}"></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="saveAcctBtn">Save Profile</button>
|
||||||
|
|
||||||
|
${user.auth_provider === 'local' ? `
|
||||||
|
<div style="border-top:1px solid var(--border);margin-top:20px;padding-top:16px">
|
||||||
|
<h4 style="font-size:14px;margin-bottom:8px">Change Password</h4>
|
||||||
|
<p style="color:var(--text-muted);font-size:12px;margin-bottom:12px">Must be at least 8 characters.</p>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
|
||||||
|
<div class="form-group"><label>Current Password</label><input type="password" id="acctCurrentPw" class="input" autocomplete="current-password"></div>
|
||||||
|
<div class="form-group"><label>New Password</label><input type="password" id="acctNewPw" class="input" autocomplete="new-password"></div>
|
||||||
|
<div class="form-group"><label>Confirm New Password</label><input type="password" id="acctConfirmPw" class="input" autocomplete="new-password"></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-sm" id="changePwBtn">Change Password</button>
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<p style="color:var(--text-muted);font-size:12px;margin-top:16px">You sign in via <strong>${esc(user.auth_provider || 'SSO')}</strong>. Manage your password there.</p>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
|
||||||
${isAdmin ? `
|
${isAdmin ? `
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>License</h3>
|
<h3>License</h3>
|
||||||
|
|
@ -255,6 +279,45 @@ export async function render(container) {
|
||||||
setLanguage(e.target.value);
|
setLanguage(e.target.value);
|
||||||
showToast('Language changed. Refresh for full effect.', 'info');
|
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() {
|
async function loadWhiteLabel() {
|
||||||
|
|
|
||||||
|
|
@ -227,12 +227,23 @@ router.get('/me', requireAuth, (req, res) => {
|
||||||
|
|
||||||
// Update current user
|
// Update current user
|
||||||
router.put('/me', requireAuth, (req, res) => {
|
router.put('/me', requireAuth, (req, res) => {
|
||||||
const { name, password } = req.body;
|
const { name, password, current_password } = req.body;
|
||||||
if (name) {
|
if (name) {
|
||||||
db.prepare('UPDATE users SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?')
|
db.prepare('UPDATE users SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?')
|
||||||
.run(name, req.user.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);
|
const hash = bcrypt.hashSync(password, 10);
|
||||||
db.prepare('UPDATE users SET password_hash = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?')
|
db.prepare('UPDATE users SET password_hash = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?')
|
||||||
.run(hash, req.user.id);
|
.run(hash, req.user.id);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue