2024-06-25 01:05:35 +02:00
|
|
|
import { getRequestHeaders } from '../script.js';
|
2024-04-09 23:01:03 +02:00
|
|
|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
2024-04-11 23:35:51 +02:00
|
|
|
import { renderTemplateAsync } from './templates.js';
|
2024-04-13 16:52:37 +02:00
|
|
|
import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
|
2024-04-08 01:38:20 +02:00
|
|
|
|
|
|
|
/**
|
2024-04-10 01:09:38 +02:00
|
|
|
* @type {import('../../src/users.js').UserViewModel} Logged in user
|
2024-04-08 01:38:20 +02:00
|
|
|
*/
|
|
|
|
export let currentUser = null;
|
2024-04-12 22:18:43 +02:00
|
|
|
export let accountsEnabled = false;
|
2024-04-08 01:38:20 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Enable or disable user account controls in the UI.
|
|
|
|
* @param {boolean} isEnabled User account controls enabled
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
|
|
|
export async function setUserControls(isEnabled) {
|
2024-04-12 22:18:43 +02:00
|
|
|
accountsEnabled = isEnabled;
|
|
|
|
|
2024-04-08 01:38:20 +02:00
|
|
|
if (!isEnabled) {
|
2024-04-12 22:18:43 +02:00
|
|
|
$('#logout_button').hide();
|
|
|
|
$('#admin_button').hide();
|
2024-04-08 01:38:20 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-04-12 22:18:43 +02:00
|
|
|
$('#logout_button').show();
|
2024-04-08 01:38:20 +02:00
|
|
|
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();
|
2024-04-12 22:18:43 +02:00
|
|
|
$('#admin_button').toggle(accountsEnabled && isAdmin());
|
2024-04-08 01:38:20 +02:00
|
|
|
} catch (error) {
|
|
|
|
console.error('Error getting current user:', error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-10 23:40:01 +02:00
|
|
|
/**
|
|
|
|
* Get a list of all users.
|
|
|
|
* @returns {Promise<import('../../src/users.js').UserViewModel[]>} Users
|
|
|
|
*/
|
2024-04-08 01:38:20 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-09 21:43:47 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 01:38:20 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-09 21:43:47 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-09 23:01:03 +02:00
|
|
|
/**
|
|
|
|
* 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 {
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('changePassword'));
|
2024-04-09 23:01:03 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-11 23:35:51 +02:00
|
|
|
/**
|
|
|
|
* Delete a user.
|
|
|
|
* @param {string} handle User handle
|
|
|
|
* @param {function} callback Success callback
|
|
|
|
*/
|
2024-04-10 00:01:32 +02:00
|
|
|
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 = '';
|
|
|
|
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('deleteUser'));
|
2024-04-10 00:01:32 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-10 02:29:38 +02:00
|
|
|
/**
|
|
|
|
* Reset a user's settings.
|
|
|
|
* @param {string} handle User handle
|
|
|
|
* @param {function} callback Success callback
|
|
|
|
*/
|
|
|
|
async function resetSettings(handle, callback) {
|
|
|
|
try {
|
2024-04-10 21:34:51 +02:00
|
|
|
let password = '';
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('resetSettings'));
|
2024-04-10 21:34:51 +02:00
|
|
|
template.find('input[name="password"]').on('input', function () {
|
|
|
|
password = String($(this).val());
|
|
|
|
});
|
2024-04-10 02:29:38 +02:00
|
|
|
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(),
|
2024-04-10 21:34:51 +02:00
|
|
|
body: JSON.stringify({ handle, password }),
|
2024-04-10 02:29:38 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-10 23:40:01 +02:00
|
|
|
/**
|
|
|
|
* 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 {
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('changeName'));
|
2024-04-10 23:40:01 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-11 00:44:48 +02:00
|
|
|
/**
|
|
|
|
* 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() {
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('snapshotsView'));
|
2024-04-11 00:44:48 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-27 10:45:09 +02:00
|
|
|
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false, allowVerticalScrolling: true });
|
2024-04-11 00:44:48 +02:00
|
|
|
template.find('.makeSnapshotButton').on('click', () => makeSnapshot(renderSnapshots));
|
|
|
|
renderSnapshots();
|
|
|
|
}
|
|
|
|
|
2024-04-12 23:11:20 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-04-10 01:09:38 +02:00
|
|
|
async function openUserProfile() {
|
|
|
|
await getCurrentUser();
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('userProfile'));
|
2024-04-10 01:09:38 +02:00
|
|
|
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);
|
2024-04-11 00:44:48 +02:00
|
|
|
template.find('.userSettingsSnapshotsButton').on('click', () => viewSettingsSnapshots());
|
2024-04-10 23:40:01 +02:00
|
|
|
template.find('.userChangeNameButton').on('click', async () => changeName(currentUser.handle, currentUser.name, async () => {
|
|
|
|
await getCurrentUser();
|
|
|
|
template.find('.userName').text(currentUser.name);
|
|
|
|
}));
|
2024-04-10 02:29:38 +02:00
|
|
|
template.find('.userChangePasswordButton').on('click', () => changePassword(currentUser.handle, async () => {
|
|
|
|
await getCurrentUser();
|
|
|
|
template.find('.hasPassword').toggle(currentUser.password);
|
|
|
|
template.find('.noPassword').toggle(!currentUser.password);
|
|
|
|
}));
|
2024-04-10 01:09:38 +02:00
|
|
|
template.find('.userBackupButton').on('click', function () {
|
|
|
|
$(this).addClass('disabled');
|
|
|
|
backupUserData(currentUser.handle, () => {
|
|
|
|
$(this).removeClass('disabled');
|
|
|
|
});
|
|
|
|
});
|
2024-04-10 02:29:38 +02:00
|
|
|
template.find('.userResetSettingsButton').on('click', () => resetSettings(currentUser.handle, () => location.reload()));
|
2024-04-12 23:11:20 +02:00
|
|
|
template.find('.userResetAllButton').on('click', () => resetEverything(() => location.reload()));
|
2024-04-13 16:52:37 +02:00
|
|
|
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);
|
|
|
|
});
|
2024-04-10 01:09:38 +02:00
|
|
|
|
2024-04-12 22:18:43 +02:00
|
|
|
if (!accountsEnabled) {
|
|
|
|
template.find('[data-require-accounts]').hide();
|
|
|
|
template.find('.accountsDisabledHint').show();
|
|
|
|
}
|
|
|
|
|
2024-04-10 23:40:01 +02:00
|
|
|
const popupOptions = {
|
|
|
|
okButton: 'Close',
|
|
|
|
wide: false,
|
|
|
|
large: false,
|
|
|
|
allowVerticalScrolling: true,
|
|
|
|
allowHorizontalScrolling: false,
|
|
|
|
};
|
|
|
|
callGenericPopup(template, POPUP_TYPE.TEXT, '', popupOptions);
|
2024-04-10 01:09:38 +02:00
|
|
|
}
|
|
|
|
|
2024-04-13 16:52:37 +02:00
|
|
|
/**
|
|
|
|
* 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));
|
2024-06-25 01:05:35 +02:00
|
|
|
const croppedImage = await callGenericPopup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropAspect: 1, cropImage: dataUrl });
|
2024-04-13 16:52:37 +02:00
|
|
|
if (!croppedImage) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await changeAvatar(handle, String(croppedImage));
|
|
|
|
|
2024-06-25 01:05:35 +02:00
|
|
|
return String(croppedImage);
|
2024-04-13 16:52:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 01:38:20 +02:00
|
|
|
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));
|
2024-04-09 21:43:47 +02:00
|
|
|
userBlock.find('.userPromoteButton').toggle(!user.admin).on('click', () => promoteUser(user.handle, renderUsers));
|
|
|
|
userBlock.find('.userDemoteButton').toggle(user.admin).on('click', () => demoteUser(user.handle, renderUsers));
|
2024-04-09 23:01:03 +02:00
|
|
|
userBlock.find('.userChangePasswordButton').on('click', () => changePassword(user.handle, renderUsers));
|
2024-04-10 00:01:32 +02:00
|
|
|
userBlock.find('.userDelete').on('click', () => deleteUser(user.handle, renderUsers));
|
2024-04-10 23:40:01 +02:00
|
|
|
userBlock.find('.userChangeNameButton').on('click', async () => changeName(user.handle, user.name, renderUsers));
|
2024-04-09 21:43:47 +02:00
|
|
|
userBlock.find('.userBackupButton').on('click', function () {
|
|
|
|
$(this).addClass('disabled').off('click');
|
|
|
|
backupUserData(user.handle, renderUsers);
|
|
|
|
});
|
2024-04-13 16:52:37 +02:00
|
|
|
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();
|
|
|
|
});
|
2024-04-08 01:38:20 +02:00
|
|
|
template.find('.usersList').append(userBlock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-11 23:35:51 +02:00
|
|
|
const template = $(await renderTemplateAsync('admin'));
|
2024-04-08 01:38:20 +02:00
|
|
|
|
|
|
|
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));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-04-11 23:35:51 +02:00
|
|
|
template.find('.createUserDisplayName').on('input', async function () {
|
|
|
|
const slug = await slugify(String($(this).val()));
|
|
|
|
template.find('.createUserHandle').val(slug);
|
|
|
|
});
|
|
|
|
|
2024-04-08 01:38:20 +02:00
|
|
|
template.find('.userCreateForm').on('submit', function (event) {
|
|
|
|
if (!(event.target instanceof HTMLFormElement)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
createUser(event.target, () => {
|
|
|
|
template.find('.manageUsersButton').trigger('click');
|
|
|
|
renderUsers();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:09:38 +02:00
|
|
|
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false, allowVerticalScrolling: true, allowHorizontalScrolling: false });
|
2024-04-08 01:38:20 +02:00
|
|
|
renderUsers();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Log out the current user.
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
|
|
|
async function logout() {
|
|
|
|
await fetch('/api/users/logout', {
|
|
|
|
method: 'POST',
|
|
|
|
headers: getRequestHeaders(),
|
|
|
|
});
|
|
|
|
|
2024-10-07 04:17:43 +02:00
|
|
|
// 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();
|
2024-04-08 01:38:20 +02:00
|
|
|
}
|
|
|
|
|
2024-04-11 23:35:51 +02:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 01:38:20 +02:00
|
|
|
jQuery(() => {
|
|
|
|
$('#logout_button').on('click', () => {
|
|
|
|
logout();
|
|
|
|
});
|
|
|
|
$('#admin_button').on('click', () => {
|
|
|
|
openAdminPanel();
|
|
|
|
});
|
2024-04-10 01:09:38 +02:00
|
|
|
$('#account_button').on('click', () => {
|
|
|
|
openUserProfile();
|
|
|
|
});
|
2024-04-08 01:38:20 +02:00
|
|
|
});
|