Make Reset account functional

This commit is contained in:
Cohee 2024-04-13 00:11:20 +03:00
parent 2e14132a20
commit 53386b35c9
8 changed files with 143 additions and 12 deletions

View File

@ -666,13 +666,15 @@ async function getSystemMessages() {
registerPromptManagerMigration(); registerPromptManagerMigration();
$(document).ajaxError(function myErrorHandler(_, xhr) { $(document).ajaxError(function myErrorHandler(_, xhr) {
// Cohee: CSRF doesn't error out in multiple tabs anymore, so this is unnecessary
/*
if (xhr.status == 403) { if (xhr.status == 403) {
toastr.warning( toastr.warning(
'doubleCsrf errors in console are NORMAL in this case. If you want to run ST in multiple tabs, start the server with --disableCsrf option.', 'doubleCsrf errors in console are NORMAL in this case. If you want to run ST in multiple tabs, start the server with --disableCsrf option.',
'Looks like you\'ve opened SillyTavern in another browser tab', 'Looks like you\'ve opened SillyTavern in another browser tab',
{ timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }, { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
); );
} } */
}); });
async function getClientVersion() { async function getClientVersion() {

View File

@ -1,7 +1,7 @@
<form action="javascript:void(0);" class="flex-container flexFlowColumn"> <form action="javascript:void(0);" class="flex-container flexFlowColumn">
<div class="currentPasswordBlock"> <div class="currentPasswordBlock">
<label data-i18n="Current Password:" for="user">Current Password:</label> <label data-i18n="Current Password:" for="user">Current Password:</label>
<input type="text" name="current" class="text_pole" placeholder="[ No password ]" autocomplete="current-password"> <input type="password" name="current" class="text_pole" placeholder="[ No password ]" autocomplete="current-password">
</div> </div>
<div class="newPasswordBlock"> <div class="newPasswordBlock">
<label data-i18n="New Password:" for="password">New Password:</label> <label data-i18n="New Password:" for="password">New Password:</label>

View File

@ -77,7 +77,7 @@
<i class="fa-fw fa-solid fa-cog warning"></i> <i class="fa-fw fa-solid fa-cog warning"></i>
<span data-i18n="Reset Settings">Reset Settings</span> <span data-i18n="Reset Settings">Reset Settings</span>
</div> </div>
<div class="userResetAllButton menu_button menu_button_icon disabled" title="Wipe all user data and reset your account to factory settings."> <div class="userResetAllButton menu_button menu_button_icon" title="Wipe all user data and reset your account to factory settings.">
<i class="fa-fw fa-solid fa-skull warning"></i> <i class="fa-fw fa-solid fa-skull warning"></i>
<span data-i18n="Reset Everything">Reset Everything</span> <span data-i18n="Reset Everything">Reset Everything</span>
</div> </div>

View File

@ -0,0 +1,18 @@
<form action="javascript:void(0);" class="flex-container flexFlowColumn">
<h3 class="neutral_warning">
This will delete all your settings and data. There will be no undo button.
Make sure you have a backup before proceeding.
</h3>
<hr>
<div>
Account reset code has been posted to the server console.
</div>
<div class="currentPasswordBlock">
<label data-i18n="Current Password:" for="user">Current Password:</label>
<input type="password" name="password" class="text_pole" placeholder="[ No password ]" autocomplete="current-password">
</div>
<div class="resetCodeBlock">
<label data-i18n="Reset Code:" for="password">Reset Code:</label>
<input type="text" name="code" class="text_pole" placeholder="XXXX">
</div>
</form>

View File

@ -597,6 +597,64 @@ async function viewSettingsSnapshots() {
renderSnapshots(); renderSnapshots();
} }
/**
* Reset everything to default.
* @param {function} callback Success callback
*/
async function resetEverything(callback) {
try {
const step1Response = await fetch('/api/users/reset-step1', {
method: 'POST',
headers: getRequestHeaders(),
});
if (!step1Response.ok) {
const data = await step1Response.json();
toastr.error(data.error || 'Unknown error', 'Failed to reset');
throw new Error('Failed to reset everything');
}
let password = '';
let code = '';
const template = $(await renderTemplateAsync('userReset'));
template.find('input[name="password"]').on('input', function () {
password = String($(this).val());
});
template.find('input[name="code"]').on('input', function () {
code = String($(this).val());
});
const confirm = await callGenericPopup(
template,
POPUP_TYPE.CONFIRM,
'',
{ okButton: 'Reset', cancelButton: 'Cancel', wide: false, large: false },
);
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
throw new Error('Reset everything cancelled');
}
const step2Response = await fetch('/api/users/reset-step2', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ password, code }),
});
if (!step2Response.ok) {
const data = await step2Response.json();
toastr.error(data.error || 'Unknown error', 'Failed to reset');
throw new Error('Failed to reset everything');
}
toastr.success('Everything reset successfully', 'Reset Everything');
callback();
} catch (error) {
console.error('Error resetting everything:', error);
}
}
async function openUserProfile() { async function openUserProfile() {
await getCurrentUser(); await getCurrentUser();
const template = $(await renderTemplateAsync('userProfile')); const template = $(await renderTemplateAsync('userProfile'));
@ -624,6 +682,7 @@ async function openUserProfile() {
}); });
}); });
template.find('.userResetSettingsButton').on('click', () => resetSettings(currentUser.handle, () => location.reload())); template.find('.userResetSettingsButton').on('click', () => resetSettings(currentUser.handle, () => location.reload()));
template.find('.userResetAllButton').on('click', () => resetEverything(() => location.reload()));
if (!accountsEnabled) { if (!accountsEnabled) {
template.find('[data-require-accounts]').hide(); template.find('[data-require-accounts]').hide();

View File

@ -2,10 +2,15 @@ const path = require('path');
const fsPromises = require('fs').promises; const fsPromises = require('fs').promises;
const storage = require('node-persist'); const storage = require('node-persist');
const express = require('express'); const express = require('express');
const crypto = require('crypto');
const { jsonParser } = require('../express-common'); const { jsonParser } = require('../express-common');
const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive } = require('../users'); const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist } = require('../users');
const { SETTINGS_FILE } = require('../constants'); const { SETTINGS_FILE } = require('../constants');
const contentManager = require('./content-manager'); const contentManager = require('./content-manager');
const { color, Cache } = require('../util');
const { checkForNewContent } = require('./content-manager');
const RESET_CACHE = new Cache(5 * 60 * 1000);
const router = express.Router(); const router = express.Router();
@ -27,7 +32,7 @@ router.post('/logout', async (request, response) => {
router.get('/me', async (request, response) => { router.get('/me', async (request, response) => {
try { try {
if (!request.user) { if (!request.user) {
return response.sendStatus(401); return response.sendStatus(403);
} }
const user = request.user.profile; const user = request.user.profile;
@ -74,7 +79,7 @@ router.post('/change-password', jsonParser, async (request, response) => {
if (!request.user.profile.admin && 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'); console.log('Change password failed: Incorrect password');
return response.status(401).json({ error: 'Incorrect password' }); return response.status(403).json({ error: 'Incorrect password' });
} }
if (request.body.newPassword) { if (request.body.newPassword) {
@ -121,7 +126,7 @@ router.post('/reset-settings', jsonParser, async (request, response) => {
if (request.user.profile.password && request.user.profile.password !== getPasswordHash(password, request.user.profile.salt)) { if (request.user.profile.password && request.user.profile.password !== getPasswordHash(password, request.user.profile.salt)) {
console.log('Reset settings failed: Incorrect password'); console.log('Reset settings failed: Incorrect password');
return response.status(401).json({ error: 'Incorrect password' }); return response.status(403).json({ error: 'Incorrect password' });
} }
const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE); const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE);
@ -165,6 +170,53 @@ router.post('/change-name', jsonParser, async (request, response) => {
} }
}); });
router.post('/reset-step1', jsonParser, async (request, response) => {
try {
const resetCode = String(crypto.randomInt(1000, 9999));
console.log();
console.log(color.magenta(`${request.user.profile.name}, your account reset code is: `) + color.red(resetCode));
console.log();
RESET_CACHE.set(request.user.profile.handle, resetCode);
return response.sendStatus(204);
} catch (error) {
console.error('Recover step 1 failed:', error);
return response.sendStatus(500);
}
});
router.post('/reset-step2', jsonParser, async (request, response) => {
try{
if (!request.body.code) {
console.log('Recover step 2 failed: Missing required fields');
return response.status(400).json({ error: 'Missing required fields' });
}
if (request.user.profile.password && request.user.profile.password !== getPasswordHash(request.body.password, request.user.profile.salt)) {
console.log('Recover step 2 failed: Incorrect password');
return response.status(400).json({ error: 'Incorrect password' });
}
const code = RESET_CACHE.get(request.user.profile.handle);
if (!code || code !== request.body.code) {
console.log('Recover step 2 failed: Incorrect code');
return response.status(400).json({ error: 'Incorrect code' });
}
console.log('Resetting account data:', request.user.profile.handle);
await fsPromises.rm(request.user.directories.root, { recursive: true, force: true });
await ensurePublicDirectoriesExist();
await checkForNewContent([request.user.directories]);
RESET_CACHE.remove(request.user.profile.handle);
return response.sendStatus(204);
} catch (error) {
console.error('Recover step 2 failed:', error);
return response.sendStatus(500);
}
});
module.exports = { module.exports = {
router, router,
}; };

View File

@ -59,7 +59,7 @@ router.post('/login', jsonParser, async (request, response) => {
if (!user) { if (!user) {
console.log('Login failed: User not found'); console.log('Login failed: User not found');
return response.status(401).json({ error: 'User not found' }); return response.status(403).json({ error: 'User not found' });
} }
if (!user.enabled) { if (!user.enabled) {
@ -70,7 +70,7 @@ router.post('/login', jsonParser, async (request, response) => {
if (user.password && user.password !== getPasswordHash(request.body.password, user.salt)) { if (user.password && user.password !== getPasswordHash(request.body.password, user.salt)) {
console.log('Login failed: Incorrect password'); console.log('Login failed: Incorrect password');
return response.status(401).json({ error: 'Incorrect password' }); return response.status(403).json({ error: 'Incorrect password' });
} }
if (!request.session) { if (!request.session) {
@ -159,7 +159,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
if (request.body.code !== mfaCode) { if (request.body.code !== mfaCode) {
await recoverLimiter.consume(ip); await recoverLimiter.consume(ip);
console.log('Recover step 2 failed: Incorrect code'); console.log('Recover step 2 failed: Incorrect code');
return response.status(401).json({ error: 'Incorrect code' }); return response.status(403).json({ error: 'Incorrect code' });
} }
if (request.body.newPassword) { if (request.body.newPassword) {

View File

@ -551,7 +551,7 @@ async function setUserDataMiddleware(request, response, next) {
*/ */
function requireLoginMiddleware(request, response, next) { function requireLoginMiddleware(request, response, next) {
if (!request.user) { if (!request.user) {
return response.sendStatus(401); return response.sendStatus(403);
} }
return next(); return next();
@ -583,7 +583,7 @@ function createRouteHandler(directoryFn) {
*/ */
function requireAdminMiddleware(request, response, next) { function requireAdminMiddleware(request, response, next) {
if (!request.user) { if (!request.user) {
return response.sendStatus(401); return response.sendStatus(403);
} }
if (request.user.profile.admin) { if (request.user.profile.admin) {