mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Customizable avatars for users
This commit is contained in:
@ -20,7 +20,7 @@ body.login .userSelect {
|
|||||||
border: 1px solid var(--SmartThemeBorderColor);
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
width: min-content;
|
width: 30%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
transition: background-color 0.15s ease-in-out;
|
transition: background-color 0.15s ease-in-out;
|
||||||
@ -28,6 +28,15 @@ body.login .userSelect {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.login .userSelect .userName,
|
||||||
|
body.login .userSelect .userHandle {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.login .userSelect:hover {
|
body.login .userSelect:hover {
|
||||||
|
@ -98,6 +98,14 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.justifySpaceEvenly {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justifySpaceAround {
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
.alignitemsflexstart {
|
.alignitemsflexstart {
|
||||||
align-items: flex-start !important;
|
align-items: flex-start !important;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
Enter Login Details
|
Enter Login Details
|
||||||
</h3>
|
</h3>
|
||||||
<div id="userListBlock" class="wide100p">
|
<div id="userListBlock" class="wide100p">
|
||||||
<div id="userList" class="flex-container justifyCenter"></div>
|
<div id="userList" class="flex-container justifySpaceEvenly"></div>
|
||||||
<div id="handleEntryBlock" style="display:none;" class="flex-container flexFlowColumn alignItemsCenter">
|
<div id="handleEntryBlock" style="display:none;" class="flex-container flexFlowColumn alignItemsCenter">
|
||||||
<input id="userHandle" class="text_pole" type="text" placeholder="User handle" autocomplete="username">
|
<input id="userHandle" class="text_pole" type="text" placeholder="User handle" autocomplete="username">
|
||||||
</div>
|
</div>
|
||||||
|
@ -7073,10 +7073,10 @@ function onScenarioOverrideRemoveClick() {
|
|||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @param {string} inputValue - Value to set the input to.
|
* @param {string} inputValue - Value to set the input to.
|
||||||
* @param {PopupOptions} options - Options for the popup.
|
* @param {PopupOptions} options - Options for the popup.
|
||||||
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup.
|
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
|
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
|
||||||
dialogueCloseStop = true;
|
dialogueCloseStop = true;
|
||||||
if (type) {
|
if (type) {
|
||||||
popup_type = type;
|
popup_type = type;
|
||||||
@ -7133,7 +7133,7 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, a
|
|||||||
crop_data = undefined;
|
crop_data = undefined;
|
||||||
|
|
||||||
$('#avatarToCrop').cropper({
|
$('#avatarToCrop').cropper({
|
||||||
aspectRatio: 2 / 3,
|
aspectRatio: cropAspect ?? 2 / 3,
|
||||||
autoCropArea: 1,
|
autoCropArea: 1,
|
||||||
viewMode: 2,
|
viewMode: 2,
|
||||||
rotatable: false,
|
rotatable: false,
|
||||||
|
@ -215,8 +215,8 @@ function configureNormalLogin(userList) {
|
|||||||
const avatarBlock = $('<div></div>').addClass('avatar');
|
const avatarBlock = $('<div></div>').addClass('avatar');
|
||||||
avatarBlock.append($('<img>').attr('src', user.avatar));
|
avatarBlock.append($('<img>').attr('src', user.avatar));
|
||||||
userBlock.append(avatarBlock);
|
userBlock.append(avatarBlock);
|
||||||
userBlock.append($('<span></span>').text(user.name));
|
userBlock.append($('<span></span>').addClass('userName').text(user.name));
|
||||||
userBlock.append($('<small></small>').text(user.handle));
|
userBlock.append($('<small></small>').addClass('userHandle').text(user.handle));
|
||||||
userBlock.on('click', () => onUserSelected(user));
|
userBlock.on('click', () => onUserSelected(user));
|
||||||
$('#userList').append(userBlock);
|
$('#userList').append(userBlock);
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,21 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<div class="userAccountTemplate template_element">
|
<div class="userAccountTemplate template_element">
|
||||||
<div class="flex-container userAccount alignItemsCenter flexGap10">
|
<div class="flex-container userAccount alignItemsCenter flexGap10">
|
||||||
<div>
|
<div class="flex-container flexFlowColumn alignItemsCenter flexNoGap">
|
||||||
<div class="avatar">
|
<div class="avatar" title="If a custom avatar is not set, the user's default persona image will be displayed.">
|
||||||
<img src="img/ai4.png" alt="avatar">
|
<img src="img/ai4.png" alt="avatar">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-container alignItemsCenter">
|
||||||
|
<div class="userAvatarChange right_menu_button" title="Set a custom avatar.">
|
||||||
|
<i class="fa-fw fa-solid fa-image"></i>
|
||||||
|
</div>
|
||||||
|
<div class="userAvatarRemove right_menu_button" title="Remove a custom avatar.">
|
||||||
|
<i class="fa-fw fa-solid fa-trash"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form>
|
||||||
|
<input type="file" class="avatarUpload" accept="image/*" hidden>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex1 flex-container flexFlowColumn flexNoGap justifyLeft">
|
<div class="flex1 flex-container flexFlowColumn flexNoGap justifyLeft">
|
||||||
<div class="flex-container flexGap10 alignItemsCenter">
|
<div class="flex-container flexGap10 alignItemsCenter">
|
||||||
|
@ -15,10 +15,21 @@
|
|||||||
Account Info
|
Account Info
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex-container flexGap10">
|
<div class="flex-container flexGap10">
|
||||||
<div>
|
<div class="flex-container flexFlowColumn alignItemsCenter flexNoGap">
|
||||||
<div class="avatar" title="To change your user avatar, select a default persona in the Persona Management menu.">
|
<div class="avatar" title="To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.">
|
||||||
<img src="img/ai4.png" alt="avatar">
|
<img src="img/ai4.png" alt="avatar">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-container alignItemsCenter">
|
||||||
|
<div class="userAvatarChange right_menu_button" title="Set your custom avatar.">
|
||||||
|
<i class="fa-fw fa-solid fa-image"></i>
|
||||||
|
</div>
|
||||||
|
<div class="userAvatarRemove right_menu_button" title="Remove your custom avatar.">
|
||||||
|
<i class="fa-fw fa-solid fa-trash"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form>
|
||||||
|
<input type="file" class="avatarUpload" accept="image/*" hidden>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex1 flex-container flexGap10">
|
<div class="flex1 flex-container flexGap10">
|
||||||
<div class="flex-container flexFlowColumn">
|
<div class="flex-container flexFlowColumn">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getRequestHeaders } from '../script.js';
|
import { callPopup, getCropPopup, getRequestHeaders } from '../script.js';
|
||||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||||
import { renderTemplateAsync } from './templates.js';
|
import { renderTemplateAsync } from './templates.js';
|
||||||
import { humanFileSize } from './utils.js';
|
import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('../../src/users.js').UserViewModel} Logged in user
|
* @type {import('../../src/users.js').UserViewModel} Logged in user
|
||||||
@ -683,6 +683,26 @@ 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()));
|
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) {
|
if (!accountsEnabled) {
|
||||||
template.find('[data-require-accounts]').hide();
|
template.find('[data-require-accounts]').hide();
|
||||||
@ -699,6 +719,48 @@ async function openUserProfile() {
|
|||||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', popupOptions);
|
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 callPopup(getCropPopup(dataUrl), 'avatarToCrop', '', { cropAspect: 1 });
|
||||||
|
if (!croppedImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await changeAvatar(handle, String(croppedImage));
|
||||||
|
|
||||||
|
return 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 openAdminPanel() {
|
||||||
async function renderUsers() {
|
async function renderUsers() {
|
||||||
const users = await getUsers();
|
const users = await getUsers();
|
||||||
@ -724,6 +786,24 @@ async function openAdminPanel() {
|
|||||||
$(this).addClass('disabled').off('click');
|
$(this).addClass('disabled').off('click');
|
||||||
backupUserData(user.handle, renderUsers);
|
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);
|
template.find('.usersList').append(userBlock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,23 +19,29 @@ const { DEFAULT_USER } = require('../constants');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/get', requireAdminMiddleware, jsonParser, async (request, response) => {
|
router.post('/get', requireAdminMiddleware, jsonParser, async (_request, response) => {
|
||||||
try {
|
try {
|
||||||
/** @type {import('../users').User[]} */
|
/** @type {import('../users').User[]} */
|
||||||
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
||||||
|
|
||||||
const viewModels = users
|
/** @type {Promise<import('../users').UserViewModel>[]} */
|
||||||
.sort((x, y) => x.created - y.created)
|
const viewModelPromises = users
|
||||||
.map(user => ({
|
.map(user => new Promise(resolve => {
|
||||||
handle: user.handle,
|
getUserAvatar(user.handle).then(avatar =>
|
||||||
name: user.name,
|
resolve({
|
||||||
avatar: getUserAvatar(user.handle),
|
handle: user.handle,
|
||||||
admin: user.admin,
|
name: user.name,
|
||||||
enabled: user.enabled,
|
avatar: avatar,
|
||||||
created: user.created,
|
admin: user.admin,
|
||||||
password: !!user.password,
|
enabled: user.enabled,
|
||||||
|
created: user.created,
|
||||||
|
password: !!user.password,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const viewModels = await Promise.all(viewModelPromises);
|
||||||
|
viewModels.sort((x, y) => (x.created ?? 0) - (y.created ?? 0));
|
||||||
return response.json(viewModels);
|
return response.json(viewModels);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('User list failed:', error);
|
console.error('User list failed:', error);
|
||||||
|
@ -4,7 +4,7 @@ const storage = require('node-persist');
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { jsonParser } = require('../express-common');
|
const { jsonParser } = require('../express-common');
|
||||||
const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist } = require('../users');
|
const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } = 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 { color, Cache } = require('../util');
|
||||||
@ -39,7 +39,7 @@ router.get('/me', async (request, response) => {
|
|||||||
const viewModel = {
|
const viewModel = {
|
||||||
handle: user.handle,
|
handle: user.handle,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
avatar: getUserAvatar(user.handle),
|
avatar: await getUserAvatar(user.handle),
|
||||||
admin: user.admin,
|
admin: user.admin,
|
||||||
password: !!user.password,
|
password: !!user.password,
|
||||||
created: user.created,
|
created: user.created,
|
||||||
@ -52,6 +52,41 @@ router.get('/me', async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/change-avatar', jsonParser, async (request, response) => {
|
||||||
|
try {
|
||||||
|
if (!request.body.handle) {
|
||||||
|
console.log('Change avatar failed: Missing required fields');
|
||||||
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.body.handle !== request.user.profile.handle && !request.user.profile.admin) {
|
||||||
|
console.log('Change avatar failed: Unauthorized');
|
||||||
|
return response.status(403).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avatar is not a data URL or not an empty string
|
||||||
|
if (!request.body.avatar.startsWith('data:image/') && request.body.avatar !== '') {
|
||||||
|
console.log('Change avatar failed: Invalid data URL');
|
||||||
|
return response.status(400).json({ error: 'Invalid data URL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('../users').User} */
|
||||||
|
const user = await storage.getItem(toKey(request.body.handle));
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log('Change avatar failed: User not found');
|
||||||
|
return response.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await storage.setItem(toAvatarKey(request.body.handle), request.body.avatar);
|
||||||
|
|
||||||
|
return response.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return response.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/change-password', jsonParser, async (request, response) => {
|
router.post('/change-password', jsonParser, async (request, response) => {
|
||||||
try {
|
try {
|
||||||
if (!request.body.handle) {
|
if (!request.body.handle) {
|
||||||
@ -185,7 +220,7 @@ router.post('/reset-step1', jsonParser, async (request, response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/reset-step2', jsonParser, async (request, response) => {
|
router.post('/reset-step2', jsonParser, async (request, response) => {
|
||||||
try{
|
try {
|
||||||
if (!request.body.code) {
|
if (!request.body.code) {
|
||||||
console.log('Recover step 2 failed: Missing required fields');
|
console.log('Recover step 2 failed: Missing required fields');
|
||||||
return response.status(400).json({ error: 'Missing required fields' });
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
|
@ -27,16 +27,24 @@ router.post('/list', async (_request, response) => {
|
|||||||
|
|
||||||
/** @type {import('../users').User[]} */
|
/** @type {import('../users').User[]} */
|
||||||
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
||||||
const viewModels = users
|
|
||||||
|
/** @type {Promise<import('../users').UserViewModel>[]} */
|
||||||
|
const viewModelPromises = users
|
||||||
.filter(x => x.enabled)
|
.filter(x => x.enabled)
|
||||||
.sort((x, y) => x.created - y.created)
|
.map(user => new Promise(async (resolve) => {
|
||||||
.map(user => ({
|
getUserAvatar(user.handle).then(avatar =>
|
||||||
handle: user.handle,
|
resolve({
|
||||||
name: user.name,
|
handle: user.handle,
|
||||||
avatar: getUserAvatar(user.handle),
|
name: user.name,
|
||||||
password: !!user.password,
|
created: user.created,
|
||||||
|
avatar: avatar,
|
||||||
|
password: !!user.password,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const viewModels = await Promise.all(viewModelPromises);
|
||||||
|
viewModels.sort((x, y) => (x.created ?? 0) - (y.created ?? 0));
|
||||||
return response.json(viewModels);
|
return response.json(viewModels);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('User list failed:', error);
|
console.error('User list failed:', error);
|
||||||
|
26
src/users.js
26
src/users.js
@ -15,6 +15,7 @@ const { getConfigValue, color, delay, setConfigValue, generateTimestamp } = requ
|
|||||||
const { readSecret, writeSecret } = require('./endpoints/secrets');
|
const { readSecret, writeSecret } = require('./endpoints/secrets');
|
||||||
|
|
||||||
const KEY_PREFIX = 'user:';
|
const KEY_PREFIX = 'user:';
|
||||||
|
const AVATAR_PREFIX = 'avatar:';
|
||||||
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||||
const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
|
const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ const STORAGE_KEYS = {
|
|||||||
* @property {string} handle - The user's short handle. Used for directories and other references
|
* @property {string} handle - The user's short handle. Used for directories and other references
|
||||||
* @property {string} name - The user's name. Displayed in the UI
|
* @property {string} name - The user's name. Displayed in the UI
|
||||||
* @property {string} avatar - The user's avatar image
|
* @property {string} avatar - The user's avatar image
|
||||||
* @property {boolean} admin - Whether the user is an admin (can manage other users)
|
* @property {boolean} [admin] - Whether the user is an admin (can manage other users)
|
||||||
* @property {boolean} password - Whether the user is password protected
|
* @property {boolean} password - Whether the user is password protected
|
||||||
* @property {boolean} [enabled] - Whether the user is enabled
|
* @property {boolean} [enabled] - Whether the user is enabled
|
||||||
* @property {number} [created] - The timestamp when the user was created
|
* @property {number} [created] - The timestamp when the user was created
|
||||||
@ -315,6 +316,15 @@ function toKey(handle) {
|
|||||||
return `${KEY_PREFIX}${handle}`;
|
return `${KEY_PREFIX}${handle}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a user handle to a storage key for avatars.
|
||||||
|
* @param {string} handle User handle
|
||||||
|
* @returns {string} The key for the avatar storage
|
||||||
|
*/
|
||||||
|
function toAvatarKey(handle) {
|
||||||
|
return `${AVATAR_PREFIX}${handle}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the user storage. Currently a no-op.
|
* Initializes the user storage. Currently a no-op.
|
||||||
* @param {string} dataRoot The root directory for user data
|
* @param {string} dataRoot The root directory for user data
|
||||||
@ -435,10 +445,19 @@ function getUserDirectories(handle) {
|
|||||||
/**
|
/**
|
||||||
* Gets the avatar URL for the provided user.
|
* Gets the avatar URL for the provided user.
|
||||||
* @param {string} handle User handle
|
* @param {string} handle User handle
|
||||||
* @returns {string} User avatar URL
|
* @returns {Promise<string>} User avatar URL
|
||||||
*/
|
*/
|
||||||
function getUserAvatar(handle) {
|
async function getUserAvatar(handle) {
|
||||||
try {
|
try {
|
||||||
|
// Check if the user has a custom avatar
|
||||||
|
const avatarKey = toAvatarKey(handle);
|
||||||
|
const avatar = await storage.getItem(avatarKey);
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to reading from files if custom avatar is not set
|
||||||
const directory = getUserDirectories(handle);
|
const directory = getUserDirectories(handle);
|
||||||
const pathToSettings = path.join(directory.root, SETTINGS_FILE);
|
const pathToSettings = path.join(directory.root, SETTINGS_FILE);
|
||||||
const settings = fs.existsSync(pathToSettings) ? JSON.parse(fs.readFileSync(pathToSettings, 'utf8')) : {};
|
const settings = fs.existsSync(pathToSettings) ? JSON.parse(fs.readFileSync(pathToSettings, 'utf8')) : {};
|
||||||
@ -665,6 +684,7 @@ router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.us
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
KEY_PREFIX,
|
KEY_PREFIX,
|
||||||
toKey,
|
toKey,
|
||||||
|
toAvatarKey,
|
||||||
initUserStorage,
|
initUserStorage,
|
||||||
ensurePublicDirectoriesExist,
|
ensurePublicDirectoriesExist,
|
||||||
getAllUserHandles,
|
getAllUserHandles,
|
||||||
|
Reference in New Issue
Block a user