mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-03 19:37:46 +01:00
434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
import { getRequestHeaders, renderTemplate } from '../script.js';
|
|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
|
|
|
/**
|
|
* @type {import('../../src/users.js').User} Logged in user
|
|
*/
|
|
export let currentUser = null;
|
|
|
|
/**
|
|
* Enable or disable user account controls in the UI.
|
|
* @param {boolean} isEnabled User account controls enabled
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function setUserControls(isEnabled) {
|
|
if (!isEnabled) {
|
|
$('#account_controls').hide();
|
|
return;
|
|
}
|
|
|
|
$('#account_controls').show();
|
|
await getCurrentUser();
|
|
}
|
|
|
|
/**
|
|
* Check if the current user is an admin.
|
|
* @returns {boolean} True if the current user is an admin
|
|
*/
|
|
function isAdmin() {
|
|
if (!currentUser) {
|
|
return false;
|
|
}
|
|
|
|
return Boolean(currentUser.admin);
|
|
}
|
|
|
|
/**
|
|
* Get the current user.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function getCurrentUser() {
|
|
try {
|
|
const response = await fetch('/api/users/me', {
|
|
headers: getRequestHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to get current user');
|
|
}
|
|
|
|
currentUser = await response.json();
|
|
$('#admin_button').toggle(isAdmin());
|
|
} catch (error) {
|
|
console.error('Error getting current user:', error);
|
|
}
|
|
}
|
|
|
|
async function getUsers() {
|
|
try {
|
|
const response = await fetch('/api/users/get', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to get users');
|
|
}
|
|
|
|
return response.json();
|
|
} catch (error) {
|
|
console.error('Error getting users:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable a user account.
|
|
* @param {string} handle User handle
|
|
* @param {function} callback Success callback
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function enableUser(handle, callback) {
|
|
try {
|
|
const response = await fetch('/api/users/enable', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ handle }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data.error || 'Unknown error', 'Failed to enable user');
|
|
throw new Error('Failed to enable user');
|
|
}
|
|
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error enabling user:', error);
|
|
}
|
|
}
|
|
|
|
async function disableUser(handle, callback) {
|
|
try {
|
|
const response = await fetch('/api/users/disable', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ handle }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data?.error || 'Unknown error', 'Failed to disable user');
|
|
throw new Error('Failed to disable user');
|
|
}
|
|
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error disabling user:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Promote a user to admin.
|
|
* @param {string} handle User handle
|
|
* @param {function} callback Success callback
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function promoteUser(handle, callback) {
|
|
try {
|
|
const response = await fetch('/api/users/promote', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ handle }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data.error || 'Unknown error', 'Failed to promote user');
|
|
throw new Error('Failed to promote user');
|
|
}
|
|
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error promoting user:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Demote a user from admin.
|
|
* @param {string} handle User handle
|
|
* @param {function} callback Success callback
|
|
*/
|
|
async function demoteUser(handle, callback) {
|
|
try {
|
|
const response = await fetch('/api/users/demote', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ handle }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data.error || 'Unknown error', 'Failed to demote user');
|
|
throw new Error('Failed to demote user');
|
|
}
|
|
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error demoting user:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new user.
|
|
* @param {HTMLFormElement} form Form element
|
|
*/
|
|
async function createUser(form, callback) {
|
|
const errors = [];
|
|
const formData = new FormData(form);
|
|
|
|
if (!formData.get('handle')) {
|
|
errors.push('Handle is required');
|
|
}
|
|
|
|
if (formData.get('password') !== formData.get('confirm')) {
|
|
errors.push('Passwords do not match');
|
|
}
|
|
|
|
if (errors.length) {
|
|
toastr.error(errors.join(', '), 'Failed to create user');
|
|
return;
|
|
}
|
|
|
|
const body = {};
|
|
formData.forEach(function (value, key) {
|
|
if (key === 'confirm') {
|
|
return;
|
|
}
|
|
if (key.startsWith('_')) {
|
|
key = key.substring(1);
|
|
}
|
|
body[key] = value;
|
|
});
|
|
|
|
try {
|
|
const response = await fetch('/api/users/create', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data.error || 'Unknown error', 'Failed to create user');
|
|
throw new Error('Failed to create user');
|
|
}
|
|
|
|
form.reset();
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error creating user:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Backup a user's data.
|
|
* @param {string} handle Handle of the user to backup
|
|
* @param {function} callback Success callback
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function backupUserData(handle, callback) {
|
|
try {
|
|
toastr.info('Please wait for the download to start.', 'Backup Requested');
|
|
const response = await fetch('/api/users/backup', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ handle }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data.error || 'Unknown error', 'Failed to backup user data');
|
|
throw new Error('Failed to backup user data');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const header = response.headers.get('Content-Disposition');
|
|
const parts = header.split(';');
|
|
const filename = parts[1].split('=')[1].replaceAll('"', '');
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error backing up user data:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 deleteUser(handle, callback) {
|
|
try {
|
|
if (handle === currentUser.handle) {
|
|
toastr.error('Cannot delete yourself', 'Failed to delete user');
|
|
throw new Error('Cannot delete yourself');
|
|
}
|
|
|
|
let purge = false;
|
|
let confirmHandle = '';
|
|
|
|
const template = $(renderTemplate('deleteUser'));
|
|
template.find('#deleteUserName').text(handle);
|
|
template.find('input[name="deleteUserData"]').on('input', function () {
|
|
purge = $(this).is(':checked');
|
|
});
|
|
template.find('input[name="deleteUserHandle"]').on('input', function () {
|
|
confirmHandle = String($(this).val());
|
|
});
|
|
|
|
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Delete', cancelButton: 'Cancel', wide: false, large: false });
|
|
|
|
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
|
throw new Error('Delete user cancelled');
|
|
}
|
|
|
|
if (handle !== confirmHandle) {
|
|
toastr.error('Handles do not match', 'Failed to delete user');
|
|
throw new Error('Handles do not match');
|
|
}
|
|
|
|
const response = await fetch('/api/users/delete', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({ handle, purge }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
toastr.error(data.error || 'Unknown error', 'Failed to delete user');
|
|
throw new Error('Failed to delete user');
|
|
}
|
|
|
|
toastr.success('User deleted successfully', 'User Deleted');
|
|
callback();
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
}
|
|
}
|
|
|
|
async function openAdminPanel() {
|
|
async function renderUsers() {
|
|
const users = await getUsers();
|
|
template.find('.usersList').empty();
|
|
for (const user of users) {
|
|
const userBlock = template.find('.userAccountTemplate .userAccount').clone();
|
|
userBlock.find('.userName').text(user.name);
|
|
userBlock.find('.userHandle').text(user.handle);
|
|
userBlock.find('.userStatus').text(user.enabled ? 'Enabled' : 'Disabled');
|
|
userBlock.find('.userRole').text(user.admin ? 'Admin' : 'User');
|
|
userBlock.find('.avatar img').attr('src', user.avatar);
|
|
userBlock.find('.hasPassword').toggle(user.password);
|
|
userBlock.find('.noPassword').toggle(!user.password);
|
|
userBlock.find('.userCreated').text(new Date(user.created).toLocaleString());
|
|
userBlock.find('.userEnableButton').toggle(!user.enabled).on('click', () => enableUser(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('.userDemoteButton').toggle(user.admin).on('click', () => demoteUser(user.handle, renderUsers));
|
|
userBlock.find('.userChangePasswordButton').on('click', () => changePassword(user.handle, renderUsers));
|
|
userBlock.find('.userDelete').on('click', () => deleteUser(user.handle, renderUsers));
|
|
userBlock.find('.userBackupButton').on('click', function () {
|
|
$(this).addClass('disabled').off('click');
|
|
backupUserData(user.handle, renderUsers);
|
|
});
|
|
template.find('.usersList').append(userBlock);
|
|
}
|
|
}
|
|
|
|
const template = $(renderTemplate('admin'));
|
|
|
|
template.find('.adminNav > button').on('click', function () {
|
|
const target = String($(this).data('target-tab'));
|
|
template.find('.navTab').each(function () {
|
|
$(this).toggle(this.classList.contains(target));
|
|
});
|
|
});
|
|
|
|
template.find('.userCreateForm').on('submit', function (event) {
|
|
if (!(event.target instanceof HTMLFormElement)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
createUser(event.target, () => {
|
|
template.find('.manageUsersButton').trigger('click');
|
|
renderUsers();
|
|
});
|
|
});
|
|
|
|
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false });
|
|
renderUsers();
|
|
}
|
|
|
|
/**
|
|
* Log out the current user.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function logout() {
|
|
await fetch('/api/users/logout', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
});
|
|
|
|
window.location.reload();
|
|
}
|
|
|
|
jQuery(() => {
|
|
$('#logout_button').on('click', () => {
|
|
logout();
|
|
});
|
|
$('#admin_button').on('click', () => {
|
|
openAdminPanel();
|
|
});
|
|
});
|