diff --git a/public/css/accounts.css b/public/css/accounts.css new file mode 100644 index 000000000..e5414bf59 --- /dev/null +++ b/public/css/accounts.css @@ -0,0 +1,5 @@ +.userAccount { + border: 1px solid var(--SmartThemeBorderColor); + padding: 5px 10px; + border-radius: 5px; +} diff --git a/public/scripts/popup.js b/public/scripts/popup.js index b793f3a66..6a1e63b4d 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -219,7 +219,7 @@ export function callGenericPopup(text, type, inputValue = '', { okButton, cancel text, type, inputValue, - { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling }, + { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cancelButton }, ); return popup.show(); } diff --git a/public/scripts/templates/admin.html b/public/scripts/templates/admin.html index 91bdc0b1e..987f7c043 100644 --- a/public/scripts/templates/admin.html +++ b/public/scripts/templates/admin.html @@ -1,10 +1,10 @@
@@ -19,41 +19,51 @@

- @userhandle +  
- Role: + Role: - Status: - Status + Status: +   - Created: - Date + Created: +  
-
-
diff --git a/public/scripts/templates/changePassword.html b/public/scripts/templates/changePassword.html new file mode 100644 index 000000000..bbebf088f --- /dev/null +++ b/public/scripts/templates/changePassword.html @@ -0,0 +1,14 @@ +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/public/scripts/user.js b/public/scripts/user.js index 31f6285f5..deb80eb50 100644 --- a/public/scripts/user.js +++ b/public/scripts/user.js @@ -1,4 +1,5 @@ -import { callPopup, getRequestHeaders, renderTemplate } from '../script.js'; +import { getRequestHeaders, renderTemplate } from '../script.js'; +import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; /** * @type {import('../../src/users.js').User} Logged in user @@ -256,6 +257,57 @@ async function backupUserData(handle, callback) { } } +/** + * Shows a popup to change a user's password. + * @param {string} handle User handle + * @param {function} callback Success callback + */ +async function changePassword(handle, callback) { + try { + const template = $(renderTemplate('changePassword')); + template.find('.currentPasswordBlock').toggle(!isAdmin()); + let newPassword = ''; + let confirmPassword = ''; + let oldPassword = ''; + template.find('input[name="current"]').on('input', function () { + oldPassword = String($(this).val()); + }); + template.find('input[name="password"]').on('input', function () { + newPassword = String($(this).val()); + }); + template.find('input[name="confirm"]').on('input', function () { + confirmPassword = String($(this).val()); + }); + const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Change', cancelButton: 'Cancel', wide: false, large: false }); + if (result === POPUP_RESULT.CANCELLED || result === POPUP_RESULT.NEGATIVE) { + throw new Error('Change password cancelled'); + } + + if (newPassword !== confirmPassword) { + toastr.error('Passwords do not match', 'Failed to change password'); + throw new Error('Passwords do not match'); + } + + const response = await fetch('/api/users/change-password', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ handle, newPassword, oldPassword }), + }); + + if (!response.ok) { + const data = await response.json(); + toastr.error(data.error || 'Unknown error', 'Failed to change password'); + throw new Error('Failed to change password'); + } + + toastr.success('Password changed successfully', 'Password Changed'); + callback(); + } + catch (error) { + console.error('Error changing password:', error); + } +} + async function openAdminPanel() { async function renderUsers() { const users = await getUsers(); @@ -274,6 +326,7 @@ async function openAdminPanel() { userBlock.find('.userDisableButton').toggle(user.enabled).on('click', () => disableUser(user.handle, renderUsers)); userBlock.find('.userPromoteButton').toggle(!user.admin).on('click', () => promoteUser(user.handle, renderUsers)); userBlock.find('.userDemoteButton').toggle(user.admin).on('click', () => demoteUser(user.handle, renderUsers)); + userBlock.find('.userChangePasswordButton').on('click', () => changePassword(user.handle, renderUsers)); userBlock.find('.userBackupButton').on('click', function () { $(this).addClass('disabled').off('click'); backupUserData(user.handle, renderUsers); @@ -303,7 +356,7 @@ async function openAdminPanel() { }); }); - callPopup(template, 'text', '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false }); + callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false }); renderUsers(); } diff --git a/public/style.css b/public/style.css index d9818d537..8953009ee 100644 --- a/public/style.css +++ b/public/style.css @@ -5,6 +5,7 @@ @import url(css/character-group-overlay.css); @import url(css/file-form.css); @import url(css/logprobs.css); +@import url(css/accounts.css); :root { --doc-height: 100%; diff --git a/src/endpoints/users-admin.js b/src/endpoints/users-admin.js index 0310d3b1d..e3e45afe2 100644 --- a/src/endpoints/users-admin.js +++ b/src/endpoints/users-admin.js @@ -1,3 +1,4 @@ +const fsPromises = require('fs').promises; const storage = require('node-persist'); const express = require('express'); const slugify = require('slugify').default; @@ -191,6 +192,33 @@ router.post('/create', requireAdminMiddleware, jsonParser, async (request, respo } }); +router.post('/delete', requireAdminMiddleware, jsonParser, async (request, response) => { + try { + if (!request.body.handle) { + console.log('Delete user failed: Missing required fields'); + return response.status(400).json({ error: 'Missing required fields' }); + } + + if (request.body.handle === request.user.profile.handle) { + console.log('Delete user failed: Cannot delete yourself'); + return response.status(400).json({ error: 'Cannot delete yourself' }); + } + + await storage.removeItem(toKey(request.body.handle)); + + if (request.body.purge) { + const directories = getUserDirectories(request.body.handle); + console.log('Deleting data directories for', request.body.handle); + await fsPromises.rm(directories.root, { recursive: true, force: true }); + } + + return response.sendStatus(204); + } catch (error) { + console.error('User delete failed:', error); + return response.sendStatus(500); + } +}); + module.exports = { router, }; diff --git a/src/endpoints/users-private.js b/src/endpoints/users-private.js index 0b95eb94c..0ed3a7d36 100644 --- a/src/endpoints/users-private.js +++ b/src/endpoints/users-private.js @@ -67,15 +67,20 @@ router.post('/change-password', jsonParser, async (request, response) => { return response.status(403).json({ error: 'User is disabled' }); } - const isAdminChange = request.user.profile.admin && request.body.handle !== request.user.profile.handle; - if (!isAdminChange && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) { + if (!request.user.profile.admin && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) { console.log('Change password failed: Incorrect password'); return response.status(401).json({ error: 'Incorrect password' }); } - const salt = getPasswordSalt(); - user.password = getPasswordHash(request.body.newPassword, salt); - user.salt = salt; + if (request.body.newPassword) { + const salt = getPasswordSalt(); + user.password = getPasswordHash(request.body.newPassword, salt); + user.salt = salt; + } else { + user.password = ''; + user.salt = ''; + } + await storage.setItem(toKey(request.body.handle), user); return response.sendStatus(204); } catch (error) {