Admin change password flow
This commit is contained in:
parent
31cc6e51b5
commit
189d096834
|
@ -0,0 +1,5 @@
|
||||||
|
.userAccount {
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
|
@ -219,7 +219,7 @@ export function callGenericPopup(text, type, inputValue = '', { okButton, cancel
|
||||||
text,
|
text,
|
||||||
type,
|
type,
|
||||||
inputValue,
|
inputValue,
|
||||||
{ okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling },
|
{ okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cancelButton },
|
||||||
);
|
);
|
||||||
return popup.show();
|
return popup.show();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<div class="adminTabs wide100p">
|
<div class="adminTabs wide100p">
|
||||||
<nav class="adminNav flex-container alignItemsCenter justifyCenter">
|
<nav class="adminNav flex-container alignItemsCenter justifyCenter">
|
||||||
<button type="button" class="manageUsersButton menu_button menu_button_icon" data-target-tab="usersList">
|
<button type="button" class="manageUsersButton menu_button menu_button_icon" data-target-tab="usersList">
|
||||||
<h4>Manage Users</h4>
|
<h4 data-i18n="Manager Users">Manage Users</h4>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="newUserButton menu_button menu_button_icon" data-target-tab="registerNewUserBlock">
|
<button type="button" class="newUserButton menu_button menu_button_icon" data-target-tab="registerNewUserBlock">
|
||||||
<h4>New User</h4>
|
<h4 data-i18n="New User">New User</h4>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="userAccountTemplate template_element">
|
<div class="userAccountTemplate template_element">
|
||||||
|
@ -19,23 +19,24 @@
|
||||||
<i class="hasPassword fa-solid fa-lock"></i>
|
<i class="hasPassword fa-solid fa-lock"></i>
|
||||||
<i class="noPassword fa-solid fa-lock-open"></i>
|
<i class="noPassword fa-solid fa-lock-open"></i>
|
||||||
<h3 class="userName margin0"></h3>
|
<h3 class="userName margin0"></h3>
|
||||||
<small class="userHandle">@userhandle</small>
|
<small class="userHandle"> </small>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container flexFlowColumn flexNoGap">
|
<div class="flex-container flexFlowColumn flexNoGap">
|
||||||
<span>
|
<span>
|
||||||
<span>Role:</span>
|
<span data-i18n="Role:">Role:</span>
|
||||||
<span class="userRole"></span>
|
<span class="userRole"></span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span>Status:</span>
|
<span data-i18n="Status:">Status:</span>
|
||||||
<span class="userStatus">Status</span>
|
<span class="userStatus"> </span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span>Created:</span>
|
<span data-i18n="Created:">Created:</span>
|
||||||
<span class="userCreated">Date</span>
|
<span class="userCreated"> </span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-container flexFlowColumn">
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<div class="userEditButton menu_button disabled">
|
<div class="userEditButton menu_button disabled">
|
||||||
<i class="fa-fw fa-solid fa-pencil"></i>
|
<i class="fa-fw fa-solid fa-pencil"></i>
|
||||||
|
@ -52,9 +53,18 @@
|
||||||
<div class="userDemoteButton menu_button" title="Demote user to regular user.">
|
<div class="userDemoteButton menu_button" title="Demote user to regular user.">
|
||||||
<i class="fa-fw fa-solid fa-arrow-down"></i>
|
<i class="fa-fw fa-solid fa-arrow-down"></i>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-container">
|
||||||
<div class="userBackupButton menu_button menu_button_icon" title="Download a backup of user data.">
|
<div class="userBackupButton menu_button menu_button_icon" title="Download a backup of user data.">
|
||||||
<i class="fa-fw fa-solid fa-download"></i>
|
<i class="fa-fw fa-solid fa-download"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="userChangePasswordButton menu_button" title="Change user password.">
|
||||||
|
<i class="fa-fw fa-solid fa-key"></i>
|
||||||
|
</div>
|
||||||
|
<div class="userDelete menu_button warning" title="Delete user account.">
|
||||||
|
<i class="fa-fw fa-solid fa-trash"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,30 +72,33 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="navTab registerNewUserBlock" style="display: none;">
|
<div class="navTab registerNewUserBlock" style="display: none;">
|
||||||
<form class="flex-container flexFlowColumn flexGap10 userCreateForm" action="javascript:void(0);">
|
<form class="flex-container flexFlowColumn flexGap10 userCreateForm" action="javascript:void(0);">
|
||||||
<h3>
|
<h3 data-i18n="Register New SillyTavern User">
|
||||||
Register New SillyTavern User
|
Register New SillyTavern User
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex-container flexNoGap">
|
<div class="flex-container flexNoGap">
|
||||||
<span>User Handle:</span>
|
<span data-i18n="User Handle:">User Handle:</span>
|
||||||
<input name="handle" class="text_pole" placeholder="Lowercase letters, numbers, and dashes only." type="text" pattern="[a-z0-9-]+">
|
<input name="handle" class="text_pole" placeholder="Lowercase letters, numbers, and dashes only." type="text" pattern="[a-z0-9-]+">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container flexNoGap">
|
<div class="flex-container flexNoGap">
|
||||||
<span>Display Name:</span>
|
<span data-i18n="Display Name:">Display Name:</span>
|
||||||
<input name="_name" class="text_pole" type="text" placeholder="Anonymous" autocomplete="username">
|
<input name="_name" class="text_pole" type="text" placeholder="Anonymous" autocomplete="username">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container flexNoGap">
|
<div class="flex-container flexNoGap">
|
||||||
<span>Password:</span>
|
<span data-i18n="Password:">Password:</span>
|
||||||
<input name="password" class="text_pole" type="password" placeholder="[ No password ]" autocomplete="new-password">
|
<input name="password" class="text_pole" type="password" placeholder="[ No password ]" autocomplete="new-password">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container flexNoGap">
|
<div class="flex-container flexNoGap">
|
||||||
<span>Confirm Password:</span>
|
<span data-i18n="Confirm Password:">Confirm Password:</span>
|
||||||
<input name="confirm" class="text_pole" type="password" placeholder="[ No password ]" autocomplete="new-password">
|
<input name="confirm" class="text_pole" type="password" placeholder="[ No password ]" autocomplete="new-password">
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span data-i18n="This will create a new subfolder...">
|
||||||
This will create a new subfolder in the /data/ directory with the user's handle as the folder name.
|
This will create a new subfolder in the /data/ directory with the user's handle as the folder name.
|
||||||
</span>
|
</span>
|
||||||
<div class="flex-container justifyCenter">
|
<div class="flex-container justifyCenter">
|
||||||
<button type="submit" class="menu_button newUserRegisterFinalizeButton">Create</div>
|
<button type="submit" class="menu_button menu_button_icon newUserRegisterFinalizeButton">
|
||||||
|
<i class="fa-fw fa-solid fa-user-plus"></i>
|
||||||
|
<span data-i18n="Create">Create</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<form action="javascript:void(0);" class="flex-container flexFlowColumn">
|
||||||
|
<div class="currentPasswordBlock">
|
||||||
|
<label data-i18n="Current Password:" for="user">Current Password:</label>
|
||||||
|
<input type="text" name="current" class="text_pole" placeholder="[ No password ]" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<div class="newPasswordBlock">
|
||||||
|
<label data-i18n="New Password:" for="password">New Password:</label>
|
||||||
|
<input type="password" name="password" class="text_pole" placeholder="[ No password ]" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="confirmPasswordBlock">
|
||||||
|
<label data-i18n="Confirm New Password:" for="confirm">Confirm New Password:</label>
|
||||||
|
<input type="password" name="confirm" class="text_pole" placeholder="[ No password ]" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -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
|
* @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 openAdminPanel() {
|
||||||
async function renderUsers() {
|
async function renderUsers() {
|
||||||
const users = await getUsers();
|
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('.userDisableButton').toggle(user.enabled).on('click', () => disableUser(user.handle, renderUsers));
|
||||||
userBlock.find('.userPromoteButton').toggle(!user.admin).on('click', () => promoteUser(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('.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 () {
|
userBlock.find('.userBackupButton').on('click', function () {
|
||||||
$(this).addClass('disabled').off('click');
|
$(this).addClass('disabled').off('click');
|
||||||
backupUserData(user.handle, renderUsers);
|
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();
|
renderUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
@import url(css/character-group-overlay.css);
|
@import url(css/character-group-overlay.css);
|
||||||
@import url(css/file-form.css);
|
@import url(css/file-form.css);
|
||||||
@import url(css/logprobs.css);
|
@import url(css/logprobs.css);
|
||||||
|
@import url(css/accounts.css);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--doc-height: 100%;
|
--doc-height: 100%;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const fsPromises = require('fs').promises;
|
||||||
const storage = require('node-persist');
|
const storage = require('node-persist');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const slugify = require('slugify').default;
|
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 = {
|
module.exports = {
|
||||||
router,
|
router,
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,15 +67,20 @@ router.post('/change-password', jsonParser, async (request, response) => {
|
||||||
return response.status(403).json({ error: 'User is disabled' });
|
return response.status(403).json({ error: 'User is disabled' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAdminChange = request.user.profile.admin && request.body.handle !== request.user.profile.handle;
|
if (!request.user.profile.admin && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {
|
||||||
if (!isAdminChange && user.password && user.password !== getPasswordHash(request.body.oldPassword, user.salt)) {
|
|
||||||
console.log('Change password failed: Incorrect password');
|
console.log('Change password failed: Incorrect password');
|
||||||
return response.status(401).json({ error: 'Incorrect password' });
|
return response.status(401).json({ error: 'Incorrect password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.body.newPassword) {
|
||||||
const salt = getPasswordSalt();
|
const salt = getPasswordSalt();
|
||||||
user.password = getPasswordHash(request.body.newPassword, salt);
|
user.password = getPasswordHash(request.body.newPassword, salt);
|
||||||
user.salt = salt;
|
user.salt = salt;
|
||||||
|
} else {
|
||||||
|
user.password = '';
|
||||||
|
user.salt = '';
|
||||||
|
}
|
||||||
|
|
||||||
await storage.setItem(toKey(request.body.handle), user);
|
await storage.setItem(toKey(request.body.handle), user);
|
||||||
return response.sendStatus(204);
|
return response.sendStatus(204);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
Loading…
Reference in New Issue