From 189d09683428ec6f66fb7f15d08f93682cdf3aed Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 10 Apr 2024 00:01:03 +0300
Subject: [PATCH] Admin change password flow
---
public/css/accounts.css | 5 ++
public/scripts/popup.js | 2 +-
public/scripts/templates/admin.html | 77 ++++++++++++--------
public/scripts/templates/changePassword.html | 14 ++++
public/scripts/user.js | 57 ++++++++++++++-
public/style.css | 1 +
src/endpoints/users-admin.js | 28 +++++++
src/endpoints/users-private.js | 15 ++--
8 files changed, 159 insertions(+), 40 deletions(-)
create mode 100644 public/css/accounts.css
create mode 100644 public/scripts/templates/changePassword.html
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) {