mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Basic account management
This commit is contained in:
@ -21,7 +21,7 @@ basicAuthUser:
|
|||||||
# Enables CORS proxy middleware
|
# Enables CORS proxy middleware
|
||||||
enableCorsProxy: false
|
enableCorsProxy: false
|
||||||
# Enable multi-user mode
|
# Enable multi-user mode
|
||||||
enableUserAccounts: true
|
enableUserAccounts: false
|
||||||
# Used to sign session cookies. Will be auto-generated if not set
|
# Used to sign session cookies. Will be auto-generated if not set
|
||||||
cookieSecret: ''
|
cookieSecret: ''
|
||||||
# Disable security checks - NOT RECOMMENDED
|
# Disable security checks - NOT RECOMMENDED
|
||||||
|
@ -88,6 +88,7 @@
|
|||||||
<script type="module" src="scripts/bulk-edit.js"></script>
|
<script type="module" src="scripts/bulk-edit.js"></script>
|
||||||
<script type="module" src="scripts/cfg-scale.js"></script>
|
<script type="module" src="scripts/cfg-scale.js"></script>
|
||||||
<script type="module" src="scripts/chats.js"></script>
|
<script type="module" src="scripts/chats.js"></script>
|
||||||
|
<script type="module" src="scripts/user.js"></script>
|
||||||
<title>SillyTavern</title>
|
<title>SillyTavern</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -3452,7 +3453,21 @@
|
|||||||
<small id="version_display"></small>
|
<small id="version_display"></small>
|
||||||
</div>
|
</div>
|
||||||
<div name="UserSettingsRowTwo" class="flex-container flexFlowRow">
|
<div name="UserSettingsRowTwo" class="flex-container flexFlowRow">
|
||||||
<textarea id="settingsSearch" class="textarea_compact wide100p" rows="1" placeholder="Search Settings" data-i18n="[placeholder]Search Settings"></textarea>
|
<div id="account_controls" class="flex-container">
|
||||||
|
<div id="account_button" class="margin0 menu_button_icon menu_button">
|
||||||
|
<i class="fa-fw fa-solid fa-user-shield"></i>
|
||||||
|
<span data-i18n="Account">Account</span>
|
||||||
|
</div>
|
||||||
|
<div id="admin_button" class="margin0 menu_button_icon menu_button" >
|
||||||
|
<i class="fa-fw fa-solid fa-user-tie"></i>
|
||||||
|
<span data-i18n="Admin Panel">Admin Panel</span>
|
||||||
|
</div>
|
||||||
|
<div id="logout_button" class="margin0 menu_button_icon menu_button">
|
||||||
|
<i class="fa-fw fa-solid fa-right-from-bracket"></i>
|
||||||
|
<span data-i18n="Logout">Logout</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="settingsSearch" class="textarea_compact flex1" rows="1" placeholder="Search Settings" data-i18n="[placeholder]Search Settings"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="user-settings-block-content" class="flex-container spaceEvenly">
|
<div id="user-settings-block-content" class="flex-container spaceEvenly">
|
||||||
@ -5277,17 +5292,16 @@
|
|||||||
Enable simple UI mode
|
Enable simple UI mode
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<h3 data-i18n="Your Persona">
|
||||||
|
Your Persona
|
||||||
|
</h3>
|
||||||
<div class="justifyLeft margin-bot-10px">
|
<div class="justifyLeft margin-bot-10px">
|
||||||
<span data-i18n="Before you get started, you must select a user name.">
|
<span data-i18n="Before you get started, you must select a persona name.">
|
||||||
Before you get started, you must select a user name.
|
Before you get started, you must select a persona name.
|
||||||
</span>
|
</span>
|
||||||
This can be changed at any time via the <code><i class="fa-solid fa-face-smile"></i></code> icon.
|
This can be changed at any time via the <code><i class="fa-solid fa-face-smile"></i></code> icon.
|
||||||
</div>
|
</div>
|
||||||
<h4 data-i18n="UI Language:">UI Language:</h4>
|
<h4 data-i18n="Persona Name:">Persona Name:</h4>
|
||||||
<select name="onboarding_ui_language">
|
|
||||||
<option value="en">English</option>
|
|
||||||
</select>
|
|
||||||
<h4 data-i18n="User Name:">User Name:</h4>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="group_member_template" class="template_element">
|
<div id="group_member_template" class="template_element">
|
||||||
|
@ -211,6 +211,7 @@ import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermati
|
|||||||
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
|
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
|
||||||
import { initPresetManager } from './scripts/preset-manager.js';
|
import { initPresetManager } from './scripts/preset-manager.js';
|
||||||
import { evaluateMacros } from './scripts/macros.js';
|
import { evaluateMacros } from './scripts/macros.js';
|
||||||
|
import { currentUser, setUserControls } from './scripts/user.js';
|
||||||
|
|
||||||
//exporting functions and vars for mods
|
//exporting functions and vars for mods
|
||||||
export {
|
export {
|
||||||
@ -6097,7 +6098,7 @@ async function doOnboarding(avatarId) {
|
|||||||
template.find('input[name="enable_simple_mode"]').on('input', function () {
|
template.find('input[name="enable_simple_mode"]').on('input', function () {
|
||||||
simpleUiMode = $(this).is(':checked');
|
simpleUiMode = $(this).is(':checked');
|
||||||
});
|
});
|
||||||
var userName = await callPopup(template, 'input', name1);
|
let userName = await callPopup(template, 'input', currentUser?.name || name1);
|
||||||
|
|
||||||
if (userName) {
|
if (userName) {
|
||||||
userName = userName.replace('\n', ' ');
|
userName = userName.replace('\n', ' ');
|
||||||
@ -6151,6 +6152,8 @@ async function getSettings() {
|
|||||||
$('#your_name').val(name1);
|
$('#your_name').val(name1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await setUserControls(data.enable_accounts);
|
||||||
|
|
||||||
// Allow subscribers to mutate settings
|
// Allow subscribers to mutate settings
|
||||||
eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
|
eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
|
||||||
|
|
||||||
|
77
public/scripts/templates/admin.html
Normal file
77
public/scripts/templates/admin.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<div class="adminTabs wide100p">
|
||||||
|
<nav class="adminNav flex-container alignItemsCenter justifyCenter">
|
||||||
|
<button type="button" class="manageUsersButton menu_button menu_button_icon" data-target-tab="usersList">
|
||||||
|
<h4>Manage Users</h4>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="newUserButton menu_button menu_button_icon" data-target-tab="registerNewUserBlock">
|
||||||
|
<h4>New User</h4>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<div class="userAccountTemplate template_element">
|
||||||
|
<div class="flex-container userAccount alignItemsCenter flexGap10">
|
||||||
|
<div>
|
||||||
|
<div class="avatar">
|
||||||
|
<img src="img/ai4.png" alt="avatar">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex1 flex-container flexFlowColumn flexNoGap justifyLeft">
|
||||||
|
<div class="flex-container flexGap10 alignItemsCenter">
|
||||||
|
<i class="hasPassword fa-solid fa-lock"></i>
|
||||||
|
<i class="noPassword fa-solid fa-lock-open"></i>
|
||||||
|
<h3 class="userName margin0"></h3>
|
||||||
|
<small class="userHandle">@userhandle</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-container flexFlowColumn flexNoGap">
|
||||||
|
<span>
|
||||||
|
<span>Role:</span>
|
||||||
|
<span class="userRole"></span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span>Status:</span>
|
||||||
|
<span class="userStatus">Status</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span>Created:</span>
|
||||||
|
<span class="userCreated">Date</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="userEditButton menu_button">Edit</div>
|
||||||
|
<div class="userDisableButton menu_button">Disable</div>
|
||||||
|
<div class="userEnableButton menu_button">Enable</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="navTab usersList flex-container flexFlowColumn">
|
||||||
|
</div>
|
||||||
|
<div class="navTab registerNewUserBlock" style="display: none;">
|
||||||
|
<form class="flex-container flexFlowColumn flexGap10 userCreateForm" action="javascript:void(0);">
|
||||||
|
<h3>
|
||||||
|
Register New SillyTavern User
|
||||||
|
</h3>
|
||||||
|
<div class="flex-container flexNoGap">
|
||||||
|
<span>User Handle:</span>
|
||||||
|
<input name="handle" class="text_pole" placeholder="Lowercase letters, numbers, and dashes only." type="text" pattern="[a-z0-9-]+">
|
||||||
|
</div>
|
||||||
|
<div class="flex-container flexNoGap">
|
||||||
|
<span>Display Name:</span>
|
||||||
|
<input name="_name" class="text_pole" type="text" placeholder="Anonymous">
|
||||||
|
</div>
|
||||||
|
<div class="flex-container flexNoGap">
|
||||||
|
<span>Password:</span>
|
||||||
|
<input name="password" class="text_pole" type="password" placeholder="[ No password ]">
|
||||||
|
</div>
|
||||||
|
<div class="flex-container flexNoGap">
|
||||||
|
<span>Confirm Password:</span>
|
||||||
|
<input name="confirm" class="text_pole" type="password" placeholder="[ No password ]">
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
This will create a new subfolder in the /data/ directory with the user's handle as the folder name.
|
||||||
|
</span>
|
||||||
|
<div class="flex-container justifyCenter">
|
||||||
|
<button type="submit" class="menu_button newUserRegisterFinalizeButton">Create</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
236
public/scripts/user.js
Normal file
236
public/scripts/user.js
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import { callPopup, getRequestHeaders, renderTemplate } from '../script.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
callPopup(template, '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();
|
||||||
|
});
|
||||||
|
});
|
@ -1,161 +0,0 @@
|
|||||||
async function registerNewUser() {
|
|
||||||
let handle = String($("#newUserHandle").val());
|
|
||||||
let name = String($("#newUserName").val());
|
|
||||||
let password = String($("#newUserPassword").val());
|
|
||||||
let passwordConfirm = String($("#newUserPasswordConfirm").val());
|
|
||||||
|
|
||||||
if (handle.length < 4) {
|
|
||||||
alert('Username must be at least 4 characters long');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 8) {
|
|
||||||
alert('Password must be at least 8 characters long');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password !== passwordConfirm) {
|
|
||||||
alert("Passwords don't match!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = {
|
|
||||||
handle: handle,
|
|
||||||
name: name || 'Anonymous',
|
|
||||||
password: password,
|
|
||||||
enabled: true
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await $.ajax({
|
|
||||||
url: '/api/users/create',
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(newUser),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(response);
|
|
||||||
if (response.handle) {
|
|
||||||
console.log('saw user created successfully')
|
|
||||||
alert('New user created!')
|
|
||||||
$("#userSelectBlock").empty()
|
|
||||||
populateUserList()
|
|
||||||
$("#userListBlock").show()
|
|
||||||
$("#registerNewUserBlock").hide()
|
|
||||||
$("#registerNewUserBlock input").val('')
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating new user:', error);
|
|
||||||
alert(error.responseText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginUser() {
|
|
||||||
const password = $("#userPassword").val();
|
|
||||||
const handle = $('.userSelect.selected').data('foruser');
|
|
||||||
const userInfo = {
|
|
||||||
handle: handle,
|
|
||||||
password: password,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await $.ajax({
|
|
||||||
url: '/api/users/login',
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(userInfo),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(response);
|
|
||||||
if (response.handle) {
|
|
||||||
console.log('successfully logged in');
|
|
||||||
alert(`logged in as ${handle}!`);
|
|
||||||
$('#loader').animate({ opacity: 0 }, 300, function () {
|
|
||||||
// Insert user handle/password verification code here
|
|
||||||
// .finally:
|
|
||||||
$('#loader').remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error logging in:', error);
|
|
||||||
alert(error.responseText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function populateUserList() {
|
|
||||||
const userList = await getUserList();
|
|
||||||
|
|
||||||
const registerNewUserButtonHTML = `<div id="registerNewUserButton" class="menu_button flex-container flexFlowCol">New User</div>`
|
|
||||||
|
|
||||||
const newUserRegisterationHTML = `
|
|
||||||
<div class="flex-container flexFlowColumn">
|
|
||||||
Register New SillyTavern User
|
|
||||||
<div class="flex-container">Username: <input id="newUserHandle" class="text_pole"></div>
|
|
||||||
<div class="flex-container">Display Name: <input id="newUserName" class="text_pole"></div>
|
|
||||||
<div class="flex-container">Password: <input id="newUserPassword" class="text_pole" type="password"></div>
|
|
||||||
<div class="flex-container">Password confirm: <input id="newUserPasswordConfirm" class="text_pole" type="password"></div>
|
|
||||||
This will create a new subfolder in the /data/ directory.
|
|
||||||
<div class="flex-container">
|
|
||||||
<div id="newUserRegisterFinalizeButton" class="menu_button">Register</div>
|
|
||||||
<div id="newUserRegisterCancelButton" class="menu_button">Cancel</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
const userSelectHTML = `
|
|
||||||
|
|
||||||
<div id="registerNewUserBlock" style="display:none;">
|
|
||||||
${newUserRegisterationHTML}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add login screen
|
|
||||||
$('#loader').append(userSelectHTML);
|
|
||||||
|
|
||||||
const parentDiv = $('#userList');
|
|
||||||
|
|
||||||
userList.forEach(user => {
|
|
||||||
const userDiv = $('<div></div>')
|
|
||||||
.attr('id', `userSelect-${user.handle}`)
|
|
||||||
.attr('data-foruser', user.name)
|
|
||||||
.addClass('userSelect menu_button flex-container flexFlowCol');
|
|
||||||
|
|
||||||
const avatarImg = $('<img>')
|
|
||||||
.addClass('avatar')
|
|
||||||
.attr('src', user.avatar);
|
|
||||||
|
|
||||||
userDiv.append(avatarImg);
|
|
||||||
|
|
||||||
const userName = $('<span></span>').text(user.name);
|
|
||||||
userDiv.append(userName);
|
|
||||||
|
|
||||||
parentDiv.append(userDiv);
|
|
||||||
});
|
|
||||||
|
|
||||||
parentDiv.append(registerNewUserButtonHTML)
|
|
||||||
|
|
||||||
$(".userSelect").off('click').on("click", function () {
|
|
||||||
let selectedUserName = $(this).data('foruser')
|
|
||||||
$('.userSelect').removeClass('avatar-container selected')
|
|
||||||
$(this).addClass('avatar-container selected')
|
|
||||||
console.log(selectedUserName)
|
|
||||||
$("#passwordHeaderText").text(`Enter password for ${selectedUserName}`)
|
|
||||||
$("#passwordEntryBlock").show()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#registerNewUserButton").off('click').on('click', function () {
|
|
||||||
$("#userListBlock").hide()
|
|
||||||
$("#registerNewUserBlock").show()
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#newUserRegisterFinalizeButton").off('click').on('click', registerNewUser)
|
|
||||||
|
|
||||||
$("#newUserRegisterCancelButton").off('click').on('click', function () {
|
|
||||||
$("#userListBlock").show()
|
|
||||||
$("#registerNewUserBlock").hide()
|
|
||||||
})
|
|
||||||
|
|
||||||
$("#loginButton").off('click').on('click', loginUser)
|
|
||||||
}
|
|
@ -8,7 +8,8 @@ const { jsonParser } = require('../express-common');
|
|||||||
const { getAllUserHandles, getUserDirectories } = require('../users');
|
const { getAllUserHandles, getUserDirectories } = require('../users');
|
||||||
|
|
||||||
const SETTINGS_FILE = 'settings.json';
|
const SETTINGS_FILE = 'settings.json';
|
||||||
const enableExtensions = getConfigValue('enableExtensions', true);
|
const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
|
||||||
|
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||||
|
|
||||||
function readAndParseFromDirectory(directoryPath, fileExtension = '.json') {
|
function readAndParseFromDirectory(directoryPath, fileExtension = '.json') {
|
||||||
const files = fs
|
const files = fs
|
||||||
@ -163,7 +164,8 @@ router.post('/get', jsonParser, (request, response) => {
|
|||||||
quickReplyPresets,
|
quickReplyPresets,
|
||||||
instruct,
|
instruct,
|
||||||
context,
|
context,
|
||||||
enable_extensions: enableExtensions,
|
enable_extensions: ENABLE_EXTENSIONS,
|
||||||
|
enable_accounts: ENABLE_ACCOUNTS,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
42
src/users.js
42
src/users.js
@ -604,7 +604,7 @@ const publicEndpoints = express.Router();
|
|||||||
|
|
||||||
publicEndpoints.get('/list', async (_request, response) => {
|
publicEndpoints.get('/list', async (_request, response) => {
|
||||||
/** @type {User[]} */
|
/** @type {User[]} */
|
||||||
const users = await storage.values();
|
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
||||||
const viewModels = users
|
const viewModels = users
|
||||||
.filter(x => x.enabled)
|
.filter(x => x.enabled)
|
||||||
.sort((x, y) => x.created - y.created)
|
.sort((x, y) => x.created - y.created)
|
||||||
@ -612,7 +612,6 @@ publicEndpoints.get('/list', async (_request, response) => {
|
|||||||
handle: user.handle,
|
handle: user.handle,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
avatar: getUserAvatar(user.handle),
|
avatar: getUserAvatar(user.handle),
|
||||||
admin: user.admin,
|
|
||||||
password: !!user.password,
|
password: !!user.password,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -672,7 +671,7 @@ publicEndpoints.post('/recover-step1', jsonParser, async (request, response) =>
|
|||||||
return response.status(403).json({ error: 'User is disabled' });
|
return response.status(403).json({ error: 'User is disabled' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mfaCode = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
const mfaCode = String(crypto.randomInt(1000, 9999));
|
||||||
console.log();
|
console.log();
|
||||||
console.log(color.blue(`${user.name}, your password recovery code is: `) + color.magenta(mfaCode));
|
console.log(color.blue(`${user.name}, your password recovery code is: `) + color.magenta(mfaCode));
|
||||||
console.log();
|
console.log();
|
||||||
@ -706,21 +705,31 @@ publicEndpoints.post('/recover-step2', jsonParser, async (request, response) =>
|
|||||||
return response.status(401).json({ error: 'Incorrect code' });
|
return response.status(401).json({ error: 'Incorrect code' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPassword = request.body.newPassword || '';
|
if (request.body.newPassword) {
|
||||||
const salt = getPasswordSalt();
|
const salt = getPasswordSalt();
|
||||||
user.password = getPasswordHash(newPassword, salt);
|
user.password = getPasswordHash(request.body.newPassword, salt);
|
||||||
user.salt = salt;
|
user.salt = salt;
|
||||||
await storage.setItem(toKey(user.handle), user);
|
await storage.setItem(toKey(user.handle), user);
|
||||||
|
} else {
|
||||||
|
user.password = '';
|
||||||
|
user.salt = '';
|
||||||
|
await storage.setItem(toKey(user.handle), user);
|
||||||
|
}
|
||||||
|
|
||||||
return response.sendStatus(204);
|
return response.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
const authenticatedEndpoints = express.Router();
|
const authenticatedEndpoints = express.Router();
|
||||||
|
|
||||||
authenticatedEndpoints.post('/logout', async (request, response) => {
|
authenticatedEndpoints.post('/logout', async (request, response) => {
|
||||||
request.session?.destroy(() => {
|
if (!request.session) {
|
||||||
|
console.error('Session not available');
|
||||||
|
return response.sendStatus(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.session.handle = null;
|
||||||
return response.sendStatus(204);
|
return response.sendStatus(204);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
authenticatedEndpoints.get('/me', async (request, response) => {
|
authenticatedEndpoints.get('/me', async (request, response) => {
|
||||||
if (!request.user) {
|
if (!request.user) {
|
||||||
@ -772,6 +781,25 @@ authenticatedEndpoints.post('/change-password', jsonParser, async (request, resp
|
|||||||
|
|
||||||
const adminEndpoints = express.Router();
|
const adminEndpoints = express.Router();
|
||||||
|
|
||||||
|
adminEndpoints.post('/get', requireAdminMiddleware, jsonParser, async (request, response) => {
|
||||||
|
/** @type {User[]} */
|
||||||
|
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
||||||
|
|
||||||
|
const viewModels = users
|
||||||
|
.sort((x, y) => x.created - y.created)
|
||||||
|
.map(user => ({
|
||||||
|
handle: user.handle,
|
||||||
|
name: user.name,
|
||||||
|
avatar: getUserAvatar(user.handle),
|
||||||
|
admin: user.admin,
|
||||||
|
enabled: user.enabled,
|
||||||
|
created: user.created,
|
||||||
|
password: !!user.password,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return response.json(viewModels);
|
||||||
|
});
|
||||||
|
|
||||||
adminEndpoints.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
|
adminEndpoints.post('/disable', requireAdminMiddleware, jsonParser, async (request, response) => {
|
||||||
if (!request.body.handle) {
|
if (!request.body.handle) {
|
||||||
console.log('Disable user failed: Missing required fields');
|
console.log('Disable user failed: Missing required fields');
|
||||||
|
Reference in New Issue
Block a user