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:
@@ -19,23 +19,29 @@ const { DEFAULT_USER } = require('../constants');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/get', requireAdminMiddleware, jsonParser, async (request, response) => {
|
||||
router.post('/get', requireAdminMiddleware, jsonParser, async (_request, response) => {
|
||||
try {
|
||||
/** @type {import('../users').User[]} */
|
||||
const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
|
||||
|
||||
const viewModels = users
|
||||
.sort((x, y) => x.created - y.created)
|
||||
.map(user => ({
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
avatar: getUserAvatar(user.handle),
|
||||
admin: user.admin,
|
||||
enabled: user.enabled,
|
||||
created: user.created,
|
||||
password: !!user.password,
|
||||
/** @type {Promise<import('../users').UserViewModel>[]} */
|
||||
const viewModelPromises = users
|
||||
.map(user => new Promise(resolve => {
|
||||
getUserAvatar(user.handle).then(avatar =>
|
||||
resolve({
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
avatar: avatar,
|
||||
admin: user.admin,
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error('User list failed:', error);
|
||||
|
@@ -4,7 +4,7 @@ const storage = require('node-persist');
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
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 contentManager = require('./content-manager');
|
||||
const { color, Cache } = require('../util');
|
||||
@@ -39,7 +39,7 @@ router.get('/me', async (request, response) => {
|
||||
const viewModel = {
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
avatar: getUserAvatar(user.handle),
|
||||
avatar: await getUserAvatar(user.handle),
|
||||
admin: user.admin,
|
||||
password: !!user.password,
|
||||
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) => {
|
||||
try {
|
||||
if (!request.body.handle) {
|
||||
@@ -185,7 +220,7 @@ router.post('/reset-step1', jsonParser, async (request, response) => {
|
||||
});
|
||||
|
||||
router.post('/reset-step2', jsonParser, async (request, response) => {
|
||||
try{
|
||||
try {
|
||||
if (!request.body.code) {
|
||||
console.log('Recover step 2 failed: 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[]} */
|
||||
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)
|
||||
.sort((x, y) => x.created - y.created)
|
||||
.map(user => ({
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
avatar: getUserAvatar(user.handle),
|
||||
password: !!user.password,
|
||||
.map(user => new Promise(async (resolve) => {
|
||||
getUserAvatar(user.handle).then(avatar =>
|
||||
resolve({
|
||||
handle: user.handle,
|
||||
name: user.name,
|
||||
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);
|
||||
} catch (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 KEY_PREFIX = 'user:';
|
||||
const AVATAR_PREFIX = 'avatar:';
|
||||
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||
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} name - The user's name. Displayed in the UI
|
||||
* @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} [enabled] - Whether the user is enabled
|
||||
* @property {number} [created] - The timestamp when the user was created
|
||||
@@ -315,6 +316,15 @@ function toKey(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.
|
||||
* @param {string} dataRoot The root directory for user data
|
||||
@@ -435,10 +445,19 @@ function getUserDirectories(handle) {
|
||||
/**
|
||||
* Gets the avatar URL for the provided user.
|
||||
* @param {string} handle User handle
|
||||
* @returns {string} User avatar URL
|
||||
* @returns {Promise<string>} User avatar URL
|
||||
*/
|
||||
function getUserAvatar(handle) {
|
||||
async function getUserAvatar(handle) {
|
||||
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 pathToSettings = path.join(directory.root, SETTINGS_FILE);
|
||||
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 = {
|
||||
KEY_PREFIX,
|
||||
toKey,
|
||||
toAvatarKey,
|
||||
initUserStorage,
|
||||
ensurePublicDirectoriesExist,
|
||||
getAllUserHandles,
|
||||
|
Reference in New Issue
Block a user