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
|
||||
enableCorsProxy: false
|
||||
# Enable multi-user mode
|
||||
enableUserAccounts: true
|
||||
enableUserAccounts: false
|
||||
# Used to sign session cookies. Will be auto-generated if not set
|
||||
cookieSecret: ''
|
||||
# Disable security checks - NOT RECOMMENDED
|
||||
|
@ -88,6 +88,7 @@
|
||||
<script type="module" src="scripts/bulk-edit.js"></script>
|
||||
<script type="module" src="scripts/cfg-scale.js"></script>
|
||||
<script type="module" src="scripts/chats.js"></script>
|
||||
<script type="module" src="scripts/user.js"></script>
|
||||
<title>SillyTavern</title>
|
||||
</head>
|
||||
|
||||
@ -3452,7 +3453,21 @@
|
||||
<small id="version_display"></small>
|
||||
</div>
|
||||
<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 id="user-settings-block-content" class="flex-container spaceEvenly">
|
||||
@ -5277,17 +5292,16 @@
|
||||
Enable simple UI mode
|
||||
</span>
|
||||
</label>
|
||||
<h3 data-i18n="Your Persona">
|
||||
Your Persona
|
||||
</h3>
|
||||
<div class="justifyLeft margin-bot-10px">
|
||||
<span data-i18n="Before you get started, you must select a user name.">
|
||||
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 persona name.
|
||||
</span>
|
||||
This can be changed at any time via the <code><i class="fa-solid fa-face-smile"></i></code> icon.
|
||||
</div>
|
||||
<h4 data-i18n="UI Language:">UI Language:</h4>
|
||||
<select name="onboarding_ui_language">
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<h4 data-i18n="User Name:">User Name:</h4>
|
||||
<h4 data-i18n="Persona Name:">Persona Name:</h4>
|
||||
</div>
|
||||
</div>
|
||||
<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 { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
|
||||
//exporting functions and vars for mods
|
||||
export {
|
||||
@ -6097,7 +6098,7 @@ async function doOnboarding(avatarId) {
|
||||
template.find('input[name="enable_simple_mode"]').on('input', function () {
|
||||
simpleUiMode = $(this).is(':checked');
|
||||
});
|
||||
var userName = await callPopup(template, 'input', name1);
|
||||
let userName = await callPopup(template, 'input', currentUser?.name || name1);
|
||||
|
||||
if (userName) {
|
||||
userName = userName.replace('\n', ' ');
|
||||
@ -6151,6 +6152,8 @@ async function getSettings() {
|
||||
$('#your_name').val(name1);
|
||||
}
|
||||
|
||||
await setUserControls(data.enable_accounts);
|
||||
|
||||
// Allow subscribers to mutate 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 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') {
|
||||
const files = fs
|
||||
@ -163,7 +164,8 @@ router.post('/get', jsonParser, (request, response) => {
|
||||
quickReplyPresets,
|
||||
instruct,
|
||||
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) => {
|
||||
/** @type {User[]} */
|
||||
const users = await storage.values();
|
||||
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
||||
const viewModels = users
|
||||
.filter(x => x.enabled)
|
||||
.sort((x, y) => x.created - y.created)
|
||||
@ -612,7 +612,6 @@ publicEndpoints.get('/list', async (_request, response) => {
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
avatar: getUserAvatar(user.handle),
|
||||
admin: user.admin,
|
||||
password: !!user.password,
|
||||
}));
|
||||
|
||||
@ -672,7 +671,7 @@ publicEndpoints.post('/recover-step1', jsonParser, async (request, response) =>
|
||||
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(color.blue(`${user.name}, your password recovery code is: `) + color.magenta(mfaCode));
|
||||
console.log();
|
||||
@ -706,21 +705,31 @@ publicEndpoints.post('/recover-step2', jsonParser, async (request, response) =>
|
||||
return response.status(401).json({ error: 'Incorrect code' });
|
||||
}
|
||||
|
||||
const newPassword = request.body.newPassword || '';
|
||||
if (request.body.newPassword) {
|
||||
const salt = getPasswordSalt();
|
||||
user.password = getPasswordHash(newPassword, salt);
|
||||
user.password = getPasswordHash(request.body.newPassword, salt);
|
||||
user.salt = salt;
|
||||
await storage.setItem(toKey(user.handle), user);
|
||||
} else {
|
||||
user.password = '';
|
||||
user.salt = '';
|
||||
await storage.setItem(toKey(user.handle), user);
|
||||
}
|
||||
|
||||
return response.sendStatus(204);
|
||||
});
|
||||
|
||||
const authenticatedEndpoints = express.Router();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
authenticatedEndpoints.get('/me', async (request, response) => {
|
||||
if (!request.user) {
|
||||
@ -772,6 +781,25 @@ authenticatedEndpoints.post('/change-password', jsonParser, async (request, resp
|
||||
|
||||
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) => {
|
||||
if (!request.body.handle) {
|
||||
console.log('Disable user failed: Missing required fields');
|
||||
|
Reference in New Issue
Block a user