mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			934 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			934 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { getRequestHeaders } from '../script.js';
 | 
						|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
 | 
						|
import { renderTemplateAsync } from './templates.js';
 | 
						|
import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * @type {import('../../src/users.js').UserViewModel} Logged in user
 | 
						|
 */
 | 
						|
export let currentUser = null;
 | 
						|
export let accountsEnabled = false;
 | 
						|
 | 
						|
// Extend the session every 30 minutes
 | 
						|
const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000;
 | 
						|
 | 
						|
/**
 | 
						|
 * Enable or disable user account controls in the UI.
 | 
						|
 * @param {boolean} isEnabled User account controls enabled
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
export async function setUserControls(isEnabled) {
 | 
						|
    accountsEnabled = isEnabled;
 | 
						|
 | 
						|
    if (!isEnabled) {
 | 
						|
        $('#logout_button').hide();
 | 
						|
        $('#admin_button').hide();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    $('#logout_button').show();
 | 
						|
    await getCurrentUser();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Check if the current user is an admin.
 | 
						|
 * @returns {boolean} True if the current user is an admin
 | 
						|
 */
 | 
						|
export function isAdmin() {
 | 
						|
    if (!accountsEnabled) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!currentUser) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return Boolean(currentUser.admin);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets the handle string of the current user.
 | 
						|
 * @returns {string} User handle
 | 
						|
 */
 | 
						|
export function getCurrentUserHandle() {
 | 
						|
    return currentUser?.handle || 'default-user';
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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(accountsEnabled && isAdmin());
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error getting current user:', error);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Get a list of all users.
 | 
						|
 * @returns {Promise<import('../../src/users.js').UserViewModel[]>} Users
 | 
						|
 */
 | 
						|
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 = $(await renderTemplateAsync('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);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Delete a user.
 | 
						|
 * @param {string} handle User handle
 | 
						|
 * @param {function} callback Success callback
 | 
						|
 */
 | 
						|
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 = $(await renderTemplateAsync('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);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Reset a user's settings.
 | 
						|
 * @param {string} handle User handle
 | 
						|
 * @param {function} callback Success callback
 | 
						|
 */
 | 
						|
async function resetSettings(handle, callback) {
 | 
						|
    try {
 | 
						|
        let password = '';
 | 
						|
        const template = $(await renderTemplateAsync('resetSettings'));
 | 
						|
        template.find('input[name="password"]').on('input', function () {
 | 
						|
            password = String($(this).val());
 | 
						|
        });
 | 
						|
        const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Reset', cancelButton: 'Cancel', wide: false, large: false });
 | 
						|
 | 
						|
        if (result !== POPUP_RESULT.AFFIRMATIVE) {
 | 
						|
            throw new Error('Reset settings cancelled');
 | 
						|
        }
 | 
						|
 | 
						|
        const response = await fetch('/api/users/reset-settings', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
            body: JSON.stringify({ handle, password }),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to reset settings');
 | 
						|
            throw new Error('Failed to reset settings');
 | 
						|
        }
 | 
						|
 | 
						|
        toastr.success('Settings reset successfully', 'Settings Reset');
 | 
						|
        callback();
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error resetting settings:', error);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Change a user's display name.
 | 
						|
 * @param {string} handle User handle
 | 
						|
 * @param {string} name Current name
 | 
						|
 * @param {function} callback Success callback
 | 
						|
 */
 | 
						|
async function changeName(handle, name, callback) {
 | 
						|
    try {
 | 
						|
        const template = $(await renderTemplateAsync('changeName'));
 | 
						|
        const result = await callGenericPopup(template, POPUP_TYPE.INPUT, name, { okButton: 'Change', cancelButton: 'Cancel', wide: false, large: false });
 | 
						|
 | 
						|
        if (!result) {
 | 
						|
            throw new Error('Change name cancelled');
 | 
						|
        }
 | 
						|
 | 
						|
        name = String(result);
 | 
						|
 | 
						|
        const response = await fetch('/api/users/change-name', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
            body: JSON.stringify({ handle, name }),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to change name');
 | 
						|
            throw new Error('Failed to change name');
 | 
						|
        }
 | 
						|
 | 
						|
        toastr.success('Name changed successfully', 'Name Changed');
 | 
						|
        callback();
 | 
						|
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error changing name:', error);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Restore a settings snapshot.
 | 
						|
 * @param {string} name Snapshot name
 | 
						|
 * @param {function} callback Success callback
 | 
						|
 */
 | 
						|
async function restoreSnapshot(name, callback) {
 | 
						|
    try {
 | 
						|
        const confirm = await callGenericPopup(
 | 
						|
            `Are you sure you want to restore the settings from "${name}"?`,
 | 
						|
            POPUP_TYPE.CONFIRM,
 | 
						|
            '',
 | 
						|
            { okButton: 'Restore', cancelButton: 'Cancel', wide: false, large: false },
 | 
						|
        );
 | 
						|
 | 
						|
        if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
 | 
						|
            throw new Error('Restore snapshot cancelled');
 | 
						|
        }
 | 
						|
 | 
						|
        const response = await fetch('/api/settings/restore-snapshot', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
            body: JSON.stringify({ name }),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to restore snapshot');
 | 
						|
            throw new Error('Failed to restore snapshot');
 | 
						|
        }
 | 
						|
 | 
						|
        callback();
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error restoring snapshot:', error);
 | 
						|
    }
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Load the content of a settings snapshot.
 | 
						|
 * @param {string} name Snapshot name
 | 
						|
 * @returns {Promise<string>} Snapshot content
 | 
						|
 */
 | 
						|
async function loadSnapshotContent(name) {
 | 
						|
    try {
 | 
						|
        const response = await fetch('/api/settings/load-snapshot', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
            body: JSON.stringify({ name }),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to load snapshot content');
 | 
						|
            throw new Error('Failed to load snapshot content');
 | 
						|
        }
 | 
						|
 | 
						|
        return response.text();
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error loading snapshot content:', error);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets a list of settings snapshots.
 | 
						|
 * @returns {Promise<Snapshot[]>} List of snapshots
 | 
						|
 * @typedef {Object} Snapshot
 | 
						|
 * @property {string} name Snapshot name
 | 
						|
 * @property {number} date Date in milliseconds
 | 
						|
 * @property {number} size File size in bytes
 | 
						|
 */
 | 
						|
async function getSnapshots() {
 | 
						|
    try {
 | 
						|
        const response = await fetch('/api/settings/get-snapshots', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to get settings snapshots');
 | 
						|
            throw new Error('Failed to get settings snapshots');
 | 
						|
        }
 | 
						|
 | 
						|
        const snapshots = await response.json();
 | 
						|
        return snapshots;
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error getting settings snapshots:', error);
 | 
						|
        return [];
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Make a snapshot of the current settings.
 | 
						|
 * @param {function} callback Success callback
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
async function makeSnapshot(callback) {
 | 
						|
    try {
 | 
						|
        const response = await fetch('/api/settings/make-snapshot', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to make snapshot');
 | 
						|
            throw new Error('Failed to make snapshot');
 | 
						|
        }
 | 
						|
 | 
						|
        toastr.success('Snapshot created successfully', 'Snapshot Created');
 | 
						|
        callback();
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error making snapshot:', error);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Open the settings snapshots view.
 | 
						|
 */
 | 
						|
async function viewSettingsSnapshots() {
 | 
						|
    const template = $(await renderTemplateAsync('snapshotsView'));
 | 
						|
    async function renderSnapshots() {
 | 
						|
        const snapshots = await getSnapshots();
 | 
						|
        template.find('.snapshotList').empty();
 | 
						|
 | 
						|
        for (const snapshot of snapshots.sort((a, b) => b.date - a.date)) {
 | 
						|
            const snapshotBlock = template.find('.snapshotTemplate .snapshot').clone();
 | 
						|
            snapshotBlock.find('.snapshotName').text(snapshot.name);
 | 
						|
            snapshotBlock.find('.snapshotDate').text(new Date(snapshot.date).toLocaleString());
 | 
						|
            snapshotBlock.find('.snapshotSize').text(humanFileSize(snapshot.size));
 | 
						|
            snapshotBlock.find('.snapshotRestoreButton').on('click', async (e) => {
 | 
						|
                e.stopPropagation();
 | 
						|
                restoreSnapshot(snapshot.name, () => location.reload());
 | 
						|
            });
 | 
						|
            snapshotBlock.find('.inline-drawer-toggle').on('click', async () => {
 | 
						|
                const contentBlock = snapshotBlock.find('.snapshotContent');
 | 
						|
                if (!contentBlock.val()) {
 | 
						|
                    const content = await loadSnapshotContent(snapshot.name);
 | 
						|
                    contentBlock.val(content);
 | 
						|
                }
 | 
						|
 | 
						|
            });
 | 
						|
            template.find('.snapshotList').append(snapshotBlock);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false, allowVerticalScrolling: true });
 | 
						|
    template.find('.makeSnapshotButton').on('click', () => makeSnapshot(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() {
 | 
						|
    await getCurrentUser();
 | 
						|
    const template = $(await renderTemplateAsync('userProfile'));
 | 
						|
    template.find('.userName').text(currentUser.name);
 | 
						|
    template.find('.userHandle').text(currentUser.handle);
 | 
						|
    template.find('.avatar img').attr('src', currentUser.avatar);
 | 
						|
    template.find('.userRole').text(currentUser.admin ? 'Admin' : 'User');
 | 
						|
    template.find('.userCreated').text(new Date(currentUser.created).toLocaleString());
 | 
						|
    template.find('.hasPassword').toggle(currentUser.password);
 | 
						|
    template.find('.noPassword').toggle(!currentUser.password);
 | 
						|
    template.find('.userSettingsSnapshotsButton').on('click', () => viewSettingsSnapshots());
 | 
						|
    template.find('.userChangeNameButton').on('click', async () => changeName(currentUser.handle, currentUser.name, async () => {
 | 
						|
        await getCurrentUser();
 | 
						|
        template.find('.userName').text(currentUser.name);
 | 
						|
    }));
 | 
						|
    template.find('.userChangePasswordButton').on('click', () => changePassword(currentUser.handle, async () => {
 | 
						|
        await getCurrentUser();
 | 
						|
        template.find('.hasPassword').toggle(currentUser.password);
 | 
						|
        template.find('.noPassword').toggle(!currentUser.password);
 | 
						|
    }));
 | 
						|
    template.find('.userBackupButton').on('click', function () {
 | 
						|
        $(this).addClass('disabled');
 | 
						|
        backupUserData(currentUser.handle, () => {
 | 
						|
            $(this).removeClass('disabled');
 | 
						|
        });
 | 
						|
    });
 | 
						|
    template.find('.userResetSettingsButton').on('click', () => resetSettings(currentUser.handle, () => location.reload()));
 | 
						|
    template.find('.userResetAllButton').on('click', () => resetEverything(() => location.reload()));
 | 
						|
    template.find('.userAvatarChange').on('click', () => template.find('.avatarUpload').trigger('click'));
 | 
						|
    template.find('.avatarUpload').on('change', async function () {
 | 
						|
        if (!(this instanceof HTMLInputElement)) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        const file = this.files[0];
 | 
						|
        if (!file) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        await cropAndUploadAvatar(currentUser.handle, file);
 | 
						|
        await getCurrentUser();
 | 
						|
        template.find('.avatar img').attr('src', currentUser.avatar);
 | 
						|
    });
 | 
						|
    template.find('.userAvatarRemove').on('click', async function () {
 | 
						|
        await changeAvatar(currentUser.handle, '');
 | 
						|
        await getCurrentUser();
 | 
						|
        template.find('.avatar img').attr('src', currentUser.avatar);
 | 
						|
    });
 | 
						|
 | 
						|
    if (!accountsEnabled) {
 | 
						|
        template.find('[data-require-accounts]').hide();
 | 
						|
        template.find('.accountsDisabledHint').show();
 | 
						|
    }
 | 
						|
 | 
						|
    const popupOptions = {
 | 
						|
        okButton: 'Close',
 | 
						|
        wide: false,
 | 
						|
        large: false,
 | 
						|
        allowVerticalScrolling: true,
 | 
						|
        allowHorizontalScrolling: false,
 | 
						|
    };
 | 
						|
    callGenericPopup(template, POPUP_TYPE.TEXT, '', popupOptions);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Crop and upload an avatar image.
 | 
						|
 * @param {string} handle User handle
 | 
						|
 * @param {File} file Avatar file
 | 
						|
 * @returns {Promise<string>}
 | 
						|
 */
 | 
						|
async function cropAndUploadAvatar(handle, file) {
 | 
						|
    const dataUrl = await getBase64Async(await ensureImageFormatSupported(file));
 | 
						|
    const croppedImage = await callGenericPopup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropAspect: 1, cropImage: dataUrl });
 | 
						|
    if (!croppedImage) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    await changeAvatar(handle, String(croppedImage));
 | 
						|
 | 
						|
    return String(croppedImage);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Change the avatar of the user.
 | 
						|
 * @param {string} handle User handle
 | 
						|
 * @param {string} avatar File to upload or base64 string
 | 
						|
 * @returns {Promise<void>} Avatar URL
 | 
						|
 */
 | 
						|
async function changeAvatar(handle, avatar) {
 | 
						|
    try {
 | 
						|
        const response = await fetch('/api/users/change-avatar', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
            body: JSON.stringify({ avatar, handle }),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            const data = await response.json();
 | 
						|
            toastr.error(data.error || 'Unknown error', 'Failed to change avatar');
 | 
						|
            return;
 | 
						|
        }
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error changing avatar:', 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('.userChangeNameButton').on('click', async () => changeName(user.handle, user.name, renderUsers));
 | 
						|
            userBlock.find('.userBackupButton').on('click', function () {
 | 
						|
                $(this).addClass('disabled').off('click');
 | 
						|
                backupUserData(user.handle, renderUsers);
 | 
						|
            });
 | 
						|
            userBlock.find('.userAvatarChange').on('click', () => userBlock.find('.avatarUpload').trigger('click'));
 | 
						|
            userBlock.find('.avatarUpload').on('change', async function () {
 | 
						|
                if (!(this instanceof HTMLInputElement)) {
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                const file = this.files[0];
 | 
						|
                if (!file) {
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                await cropAndUploadAvatar(user.handle, file);
 | 
						|
                renderUsers();
 | 
						|
            });
 | 
						|
            userBlock.find('.userAvatarRemove').on('click', async function () {
 | 
						|
                await changeAvatar(user.handle, '');
 | 
						|
                renderUsers();
 | 
						|
            });
 | 
						|
            template.find('.usersList').append(userBlock);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const template = $(await renderTemplateAsync('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('.createUserDisplayName').on('input', async function () {
 | 
						|
        const slug = await slugify(String($(this).val()));
 | 
						|
        template.find('.createUserHandle').val(slug);
 | 
						|
    });
 | 
						|
 | 
						|
    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: false, large: false, 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(),
 | 
						|
    });
 | 
						|
 | 
						|
    // On an explicit logout stop auto login
 | 
						|
    // to allow user to change username even
 | 
						|
    // when auto auth (such as authelia or basic)
 | 
						|
    // would be valid
 | 
						|
    const urlParams = new URLSearchParams(window.location.search);
 | 
						|
    urlParams.set('noauto', 'true');
 | 
						|
 | 
						|
    window.location.search = urlParams.toString();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Runs a text through the slugify API endpoint.
 | 
						|
 * @param {string} text Text to slugify
 | 
						|
 * @returns {Promise<string>} Slugified text
 | 
						|
 */
 | 
						|
async function slugify(text) {
 | 
						|
    try {
 | 
						|
        const response = await fetch('/api/users/slugify', {
 | 
						|
            method: 'POST',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
            body: JSON.stringify({ text }),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            throw new Error('Failed to slugify text');
 | 
						|
        }
 | 
						|
 | 
						|
        return response.text();
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Error slugifying text:', error);
 | 
						|
        return text;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Pings the server to extend the user session.
 | 
						|
 */
 | 
						|
async function extendUserSession() {
 | 
						|
    try {
 | 
						|
        const response = await fetch('/api/ping?extend=1', {
 | 
						|
            method: 'GET',
 | 
						|
            headers: getRequestHeaders(),
 | 
						|
        });
 | 
						|
 | 
						|
        if (!response.ok) {
 | 
						|
            throw new Error('Ping did not succeed', { cause: response.status });
 | 
						|
        }
 | 
						|
    } catch (error) {
 | 
						|
        console.error('Failed to extend user session', error);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
jQuery(() => {
 | 
						|
    $('#logout_button').on('click', () => {
 | 
						|
        logout();
 | 
						|
    });
 | 
						|
    $('#admin_button').on('click', () => {
 | 
						|
        openAdminPanel();
 | 
						|
    });
 | 
						|
    $('#account_button').on('click', () => {
 | 
						|
        openUserProfile();
 | 
						|
    });
 | 
						|
    setInterval(async () => {
 | 
						|
        if (currentUser) {
 | 
						|
            await extendUserSession();
 | 
						|
        }
 | 
						|
    }, SESSION_EXTEND_INTERVAL);
 | 
						|
});
 |