mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Add user management endpoints
This commit is contained in:
@ -1,10 +1,12 @@
|
|||||||
# -- NETWORK CONFIGURATION --
|
# -- DATA CONFIGURATION --
|
||||||
# Root directory for user data storage
|
# Root directory for user data storage
|
||||||
dataRoot: ./data
|
dataRoot: ./data
|
||||||
|
# -- SERVER CONFIGURATION --
|
||||||
# Listen for incoming connections
|
# Listen for incoming connections
|
||||||
listen: false
|
listen: false
|
||||||
# Server port
|
# Server port
|
||||||
port: 8000
|
port: 8000
|
||||||
|
# -- SECURITY CONFIGURATION --
|
||||||
# Toggle whitelist mode
|
# Toggle whitelist mode
|
||||||
whitelistMode: true
|
whitelistMode: true
|
||||||
# Whitelist of allowed IP addresses
|
# Whitelist of allowed IP addresses
|
||||||
@ -18,6 +20,8 @@ basicAuthUser:
|
|||||||
password: "password"
|
password: "password"
|
||||||
# Enables CORS proxy middleware
|
# Enables CORS proxy middleware
|
||||||
enableCorsProxy: false
|
enableCorsProxy: false
|
||||||
|
# Enable multi-user mode
|
||||||
|
enableUserAccounts: true
|
||||||
# Used to sign session cookies. Will be auto-generated if not set
|
# Used to sign session cookies. Will be auto-generated if not set
|
||||||
cookieSecret: ''
|
cookieSecret: ''
|
||||||
# Disable security checks - NOT RECOMMENDED
|
# Disable security checks - NOT RECOMMENDED
|
||||||
|
19
package-lock.json
generated
19
package-lock.json
generated
@ -41,6 +41,8 @@
|
|||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sillytavern-transformers": "^2.14.6",
|
"sillytavern-transformers": "^2.14.6",
|
||||||
"simple-git": "^3.19.1",
|
"simple-git": "^3.19.1",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
"vectra": "^0.2.2",
|
"vectra": "^0.2.2",
|
||||||
"wavefile": "^11.0.0",
|
"wavefile": "^11.0.0",
|
||||||
"write-file-atomic": "^5.0.1",
|
"write-file-atomic": "^5.0.1",
|
||||||
@ -3510,6 +3512,14 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/slugify": {
|
||||||
|
"version": "1.6.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
|
||||||
|
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -3703,8 +3713,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.1",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sillytavern-transformers": "^2.14.6",
|
"sillytavern-transformers": "^2.14.6",
|
||||||
"simple-git": "^3.19.1",
|
"simple-git": "^3.19.1",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
"vectra": "^0.2.2",
|
"vectra": "^0.2.2",
|
||||||
"wavefile": "^11.0.0",
|
"wavefile": "^11.0.0",
|
||||||
"write-file-atomic": "^5.0.1",
|
"write-file-atomic": "^5.0.1",
|
||||||
|
24
server.js
24
server.js
@ -35,8 +35,6 @@ util.inspect.defaultOptions.depth = 4;
|
|||||||
const {
|
const {
|
||||||
initUserStorage,
|
initUserStorage,
|
||||||
userDataMiddleware,
|
userDataMiddleware,
|
||||||
getUserDirectories,
|
|
||||||
getAllUserHandles,
|
|
||||||
migrateUserData,
|
migrateUserData,
|
||||||
getCsrfSecret,
|
getCsrfSecret,
|
||||||
getCookieSecret,
|
getCookieSecret,
|
||||||
@ -120,7 +118,7 @@ const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
|
|||||||
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
|
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
|
||||||
const basicAuthMode = getConfigValue('basicAuthMode', false);
|
const basicAuthMode = getConfigValue('basicAuthMode', false);
|
||||||
|
|
||||||
const { UPLOADS_PATH, PUBLIC_DIRECTORIES } = require('./src/constants');
|
const { UPLOADS_PATH } = require('./src/constants');
|
||||||
|
|
||||||
// CORS Settings //
|
// CORS Settings //
|
||||||
const CORS = cors({
|
const CORS = cors({
|
||||||
@ -476,7 +474,7 @@ const setupTasks = async function () {
|
|||||||
// in any order for encapsulation reasons, but right now it's unknown if that would break anything.
|
// in any order for encapsulation reasons, but right now it's unknown if that would break anything.
|
||||||
await initUserStorage();
|
await initUserStorage();
|
||||||
await settingsEndpoint.init();
|
await settingsEndpoint.init();
|
||||||
ensurePublicDirectoriesExist();
|
await contentManager.ensurePublicDirectoriesExist();
|
||||||
await migrateUserData();
|
await migrateUserData();
|
||||||
contentManager.checkForNewContent();
|
contentManager.checkForNewContent();
|
||||||
await ensureThumbnailCache();
|
await ensureThumbnailCache();
|
||||||
@ -567,21 +565,3 @@ if (cliArguments.ssl) {
|
|||||||
setupTasks,
|
setupTasks,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePublicDirectoriesExist() {
|
|
||||||
for (const dir of Object.values(PUBLIC_DIRECTORIES)) {
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userHandles = await getAllUserHandles();
|
|
||||||
for (const handle of userHandles) {
|
|
||||||
const userDirectories = getUserDirectories(handle);
|
|
||||||
for (const dir of Object.values(userDirectories)) {
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -5,6 +5,8 @@ const PUBLIC_DIRECTORIES = {
|
|||||||
extensions: 'public/scripts/extensions',
|
extensions: 'public/scripts/extensions',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_AVATAR = '/img/ai4.png';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('./users').UserDirectoryList}
|
* @type {import('./users').UserDirectoryList}
|
||||||
* @readonly
|
* @readonly
|
||||||
@ -52,6 +54,7 @@ const DEFAULT_USER = Object.freeze({
|
|||||||
password: '',
|
password: '',
|
||||||
admin: true,
|
admin: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
salt: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UNSAFE_EXTENSIONS = [
|
const UNSAFE_EXTENSIONS = [
|
||||||
@ -296,6 +299,7 @@ const OPENROUTER_KEYS = [
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DEFAULT_USER,
|
DEFAULT_USER,
|
||||||
|
DEFAULT_AVATAR,
|
||||||
PUBLIC_DIRECTORIES,
|
PUBLIC_DIRECTORIES,
|
||||||
USER_DIRECTORY_TEMPLATE,
|
USER_DIRECTORY_TEMPLATE,
|
||||||
UNSAFE_EXTENSIONS,
|
UNSAFE_EXTENSIONS,
|
||||||
|
@ -9,6 +9,35 @@ const contentDirectory = path.join(process.cwd(), 'default/content');
|
|||||||
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
||||||
const { getAllUserHandles, getUserDirectories } = require('../users');
|
const { getAllUserHandles, getUserDirectories } = require('../users');
|
||||||
const characterCardParser = require('../character-card-parser.js');
|
const characterCardParser = require('../character-card-parser.js');
|
||||||
|
const { PUBLIC_DIRECTORIES } = require('../constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ContentItem
|
||||||
|
* @property {string} filename
|
||||||
|
* @property {string} type
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the content directories exist.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function ensurePublicDirectoriesExist() {
|
||||||
|
for (const dir of Object.values(PUBLIC_DIRECTORIES)) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userHandles = await getAllUserHandles();
|
||||||
|
for (const handle of userHandles) {
|
||||||
|
const userDirectories = getUserDirectories(handle);
|
||||||
|
for (const dir of Object.values(userDirectories)) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the default presets from the content directory.
|
* Gets the default presets from the content directory.
|
||||||
@ -58,17 +87,12 @@ function getDefaultPresetFile(filename) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkForNewContent() {
|
/**
|
||||||
try {
|
* Seeds content for a user.
|
||||||
if (getConfigValue('skipContentCheck', false)) {
|
* @param {ContentItem[]} contentIndex Content index
|
||||||
return;
|
* @param {string} userHandle User handle
|
||||||
}
|
*/
|
||||||
|
async function seedContentForUser(contentIndex, userHandle) {
|
||||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
|
||||||
const contentIndex = JSON.parse(contentIndexText);
|
|
||||||
const userHandles = await getAllUserHandles();
|
|
||||||
|
|
||||||
for (const userHandle of userHandles) {
|
|
||||||
const directories = getUserDirectories(userHandle);
|
const directories = getUserDirectories(userHandle);
|
||||||
|
|
||||||
if (!fs.existsSync(directories.root)) {
|
if (!fs.existsSync(directories.root)) {
|
||||||
@ -112,6 +136,30 @@ async function checkForNewContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(contentLogPath, contentLog.join('\n'));
|
fs.writeFileSync(contentLogPath, contentLog.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for new content and seeds it for all users.
|
||||||
|
* @param {string} [userHandle] User to check the content for (optional)
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function checkForNewContent(userHandle) {
|
||||||
|
try {
|
||||||
|
if (getConfigValue('skipContentCheck', false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||||
|
const contentIndex = JSON.parse(contentIndexText);
|
||||||
|
const userHandles = await getAllUserHandles();
|
||||||
|
|
||||||
|
if (userHandle && userHandles.includes(userHandle)) {
|
||||||
|
await seedContentForUser(contentIndex, userHandle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const userHandle of userHandles) {
|
||||||
|
await seedContentForUser(contentIndex, userHandle);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Content check failed', err);
|
console.log('Content check failed', err);
|
||||||
@ -461,6 +509,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
ensurePublicDirectoriesExist,
|
||||||
checkForNewContent,
|
checkForNewContent,
|
||||||
getDefaultPresets,
|
getDefaultPresets,
|
||||||
getDefaultPresetFile,
|
getDefaultPresetFile,
|
||||||
|
227
src/users.js
227
src/users.js
@ -2,12 +2,18 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const storage = require('node-persist');
|
const storage = require('node-persist');
|
||||||
const { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES } = require('./constants');
|
const uuid = require('uuid');
|
||||||
const { getConfigValue, color, delay, setConfigValue } = require('./util');
|
const mime = require('mime-types');
|
||||||
|
const slugify = require('slugify').default;
|
||||||
|
const { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, DEFAULT_AVATAR } = require('./constants');
|
||||||
|
const { getConfigValue, color, delay, setConfigValue, Cache } = require('./util');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { readSecret, writeSecret } = require('./endpoints/secrets');
|
const { readSecret, writeSecret } = require('./endpoints/secrets');
|
||||||
|
const { jsonParser } = require('./express-common');
|
||||||
|
const contentManager = require('./endpoints/content-manager');
|
||||||
|
|
||||||
const DATA_ROOT = getConfigValue('dataRoot', './data');
|
const DATA_ROOT = getConfigValue('dataRoot', './data');
|
||||||
|
const MFA_CACHE = new Cache(5 * 60 * 1000);
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
users: 'users',
|
users: 'users',
|
||||||
@ -22,10 +28,20 @@ const STORAGE_KEYS = {
|
|||||||
* @property {string} name - The user's name. Displayed in the UI
|
* @property {string} name - The user's name. Displayed in the UI
|
||||||
* @property {number} created - The timestamp when the user was created
|
* @property {number} created - The timestamp when the user was created
|
||||||
* @property {string} password - SHA256 hash of the user's password
|
* @property {string} password - SHA256 hash of the user's password
|
||||||
|
* @property {string} salt - Salt used for hashing the password
|
||||||
* @property {boolean} enabled - Whether the user is enabled
|
* @property {boolean} enabled - Whether the user is enabled
|
||||||
* @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)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} UserViewModel
|
||||||
|
* @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} password - Whether the user is password protected
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} UserDirectoryList
|
* @typedef {Object} UserDirectoryList
|
||||||
* @property {string} root - The root directory for the user
|
* @property {string} root - The root directory for the user
|
||||||
@ -284,6 +300,10 @@ function getCookieSecret() {
|
|||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPasswordSalt() {
|
||||||
|
return crypto.randomBytes(16).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the CSRF secret from the storage.
|
* Get the CSRF secret from the storage.
|
||||||
* @param {import('express').Request} [request] HTTP request object
|
* @param {import('express').Request} [request] HTTP request object
|
||||||
@ -314,11 +334,12 @@ async function getCurrentUserHandle(_req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of all user handles. Currently hard coded to return the default user's handle.
|
* Gets a list of all user handles.
|
||||||
* @returns {Promise<string[]>} - The list of user handles
|
* @returns {Promise<string[]>} - The list of user handles
|
||||||
*/
|
*/
|
||||||
async function getAllUserHandles() {
|
async function getAllUserHandles() {
|
||||||
return [DEFAULT_USER.handle];
|
const users = await storage.getItem(STORAGE_KEYS.users);
|
||||||
|
return users.map(user => user.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -383,6 +404,26 @@ function createRouteHandler(directoryFn) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the current user is an admin.
|
||||||
|
* @param {import('express').Request} request Request object
|
||||||
|
* @param {import('express').Response} response Response object
|
||||||
|
* @param {import('express').NextFunction} next Next function
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
function requireAdminMiddleware(request, response, next) {
|
||||||
|
if (!request.user) {
|
||||||
|
return response.sendStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.user.profile.admin) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Unauthorized access to admin endpoint:', request.originalUrl);
|
||||||
|
return response.sendStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Express router for serving files from the user's directories.
|
* Express router for serving files from the user's directories.
|
||||||
*/
|
*/
|
||||||
@ -395,6 +436,184 @@ router.use('/user/images/*', createRouteHandler(req => req.user.directories.user
|
|||||||
router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
|
router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
|
||||||
router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
|
router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
|
||||||
|
|
||||||
|
const endpoints = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hashes a password using SHA256.
|
||||||
|
* @param {string} password Password to hash
|
||||||
|
* @param {string} salt Salt to use for hashing
|
||||||
|
* @returns {string} Hashed password
|
||||||
|
*/
|
||||||
|
function getPasswordHash(password, salt) {
|
||||||
|
return crypto.createHash('sha256').update(password + salt).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints.get('/list', async (_request, response) => {
|
||||||
|
/** @type {User[]} */
|
||||||
|
const users = await storage.getItem(STORAGE_KEYS.users);
|
||||||
|
const viewModels = users.filter(x => x.enabled).map(user => ({
|
||||||
|
handle: user.handle,
|
||||||
|
name: user.name,
|
||||||
|
avatar: DEFAULT_AVATAR,
|
||||||
|
admin: user.admin,
|
||||||
|
password: !!user.password,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Load avatars for each user
|
||||||
|
for (const user of viewModels) {
|
||||||
|
try {
|
||||||
|
const directory = getUserDirectories(user.handle);
|
||||||
|
const pathToSettings = path.join(directory.root, 'settings.json');
|
||||||
|
const settings = fs.existsSync(pathToSettings) ? JSON.parse(fs.readFileSync(pathToSettings, 'utf8')) : {};
|
||||||
|
const avatarFile = settings?.power_user?.default_persona || settings?.user_avatar;
|
||||||
|
if (!avatarFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const avatarPath = path.join(directory.avatars, avatarFile);
|
||||||
|
if (!fs.existsSync(avatarPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mimeType = mime.lookup(avatarPath);
|
||||||
|
const base64Content = fs.readFileSync(avatarPath, 'base64');
|
||||||
|
user.avatar = `data:${mimeType};base64,${base64Content}`;
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json(viewModels);
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.post('/recover-step1', jsonParser, async (request, response) => {
|
||||||
|
if (!request.body.handle) {
|
||||||
|
console.log('Recover step 1 failed: Missing required fields');
|
||||||
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {User[]} */
|
||||||
|
const users = await storage.getItem(STORAGE_KEYS.users);
|
||||||
|
const user = users.find(user => user.handle === request.body.handle);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log('Recover step 1 failed: User not found');
|
||||||
|
return response.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.enabled) {
|
||||||
|
console.log('Recover step 1 failed: User is disabled');
|
||||||
|
return response.status(403).json({ error: 'User is disabled' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mfaCode = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
||||||
|
console.log(color.blue(`${user.name} YOUR PASSWORD RECOVERY CODE IS: `) + color.magenta(mfaCode));
|
||||||
|
MFA_CACHE.set(user.handle, mfaCode);
|
||||||
|
return response.sendStatus(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.post('/recover-step2', jsonParser, async (request, response) => {
|
||||||
|
if (!request.body.handle || !request.body.code || !request.body.password) {
|
||||||
|
console.log('Recover step 2 failed: Missing required fields');
|
||||||
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {User[]} */
|
||||||
|
const users = await storage.getItem(STORAGE_KEYS.users);
|
||||||
|
const user = users.find(user => user.handle === request.body.handle);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log('Recover step 2 failed: User not found');
|
||||||
|
return response.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.enabled) {
|
||||||
|
console.log('Recover step 2 failed: User is disabled');
|
||||||
|
return response.status(403).json({ error: 'User is disabled' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mfaCode = MFA_CACHE.get(user.handle);
|
||||||
|
|
||||||
|
if (request.body.code !== mfaCode) {
|
||||||
|
console.log('Recover step 2 failed: Incorrect code');
|
||||||
|
return response.status(401).json({ error: 'Incorrect code' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = getPasswordSalt();
|
||||||
|
user.password = getPasswordHash(request.body.password, salt);
|
||||||
|
user.salt = salt;
|
||||||
|
await storage.setItem(STORAGE_KEYS.users, users);
|
||||||
|
return response.sendStatus(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.post('/login', jsonParser, async (request, response) => {
|
||||||
|
if (!request.body.handle || !request.body.password) {
|
||||||
|
console.log('Login failed: Missing required fields');
|
||||||
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {User[]} */
|
||||||
|
const users = await storage.getItem(STORAGE_KEYS.users);
|
||||||
|
const user = users.find(user => user.handle === request.body.handle);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log('Login failed: User not found');
|
||||||
|
return response.status(401).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.enabled) {
|
||||||
|
console.log('Login failed: User is disabled');
|
||||||
|
return response.status(403).json({ error: 'User is disabled' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.password !== getPasswordHash(request.body.password, user.salt)) {
|
||||||
|
console.log('Login failed: Incorrect password');
|
||||||
|
return response.status(401).json({ error: 'Incorrect password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Login successful:', user.handle);
|
||||||
|
return response.json({ handle: user.handle });
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.post('/create', requireAdminMiddleware, jsonParser, async (request, response) => {
|
||||||
|
if (!request.body.handle || !request.body.name) {
|
||||||
|
console.log('Create user failed: Missing required fields');
|
||||||
|
return response.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {User[]} */
|
||||||
|
const users = await storage.getItem(STORAGE_KEYS.users);
|
||||||
|
const handle = slugify(request.body.handle, { lower: true, trim: true });
|
||||||
|
|
||||||
|
if (users.some(user => user.handle === request.body.handle)) {
|
||||||
|
console.log('Create user failed: User with that handle already exists');
|
||||||
|
return response.status(409).json({ error: 'User already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = getPasswordSalt();
|
||||||
|
const password = request.body.password ? getPasswordHash(request.body.password, salt) : '';
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
uuid: uuid.v4(),
|
||||||
|
handle: handle,
|
||||||
|
name: request.body.name || 'Anonymous',
|
||||||
|
created: Date.now(),
|
||||||
|
password: password,
|
||||||
|
salt: salt,
|
||||||
|
admin: !!request.body.admin,
|
||||||
|
enabled: !!request.body.enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
users.push(newUser);
|
||||||
|
await storage.setItem(STORAGE_KEYS.users, users);
|
||||||
|
|
||||||
|
// Create user directories
|
||||||
|
console.log('Creating data directories for', newUser.handle);
|
||||||
|
await contentManager.ensurePublicDirectoriesExist();
|
||||||
|
await contentManager.checkForNewContent(newUser.handle);
|
||||||
|
return response.json({ handle: newUser.handle });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/api/users', endpoints);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initUserStorage,
|
initUserStorage,
|
||||||
getCurrentUserDirectories,
|
getCurrentUserDirectories,
|
||||||
|
Reference in New Issue
Block a user