|
|
|
import path from 'node:path'; |
|
import fs from 'node:fs'; |
|
import crypto from 'node:crypto'; |
|
import os from 'node:os'; |
|
import process from 'node:process'; |
|
import { Buffer } from 'node:buffer'; |
|
|
|
|
|
import storage from 'node-persist'; |
|
import express from 'express'; |
|
import mime from 'mime-types'; |
|
import archiver from 'archiver'; |
|
import _ from 'lodash'; |
|
import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
|
|
|
import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
|
import { getConfigValue, color, delay, generateTimestamp } from './util.js'; |
|
import { readSecret, writeSecret } from './endpoints/secrets.js'; |
|
import { getContentOfType } from './endpoints/content-manager.js'; |
|
import { serverDirectory } from './server-directory.js'; |
|
|
|
export const KEY_PREFIX = 'user:'; |
|
const AVATAR_PREFIX = 'avatar:'; |
|
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); |
|
const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false, 'boolean'); |
|
const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); |
|
const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); |
|
|
|
|
|
|
|
|
|
|
|
const DIRECTORIES_CACHE = new Map(); |
|
const PUBLIC_USER_AVATAR = '/img/default-user.png'; |
|
const COOKIE_SECRET_PATH = 'cookie-secret.txt'; |
|
|
|
const STORAGE_KEYS = { |
|
csrfSecret: 'csrfSecret', |
|
|
|
|
|
|
|
cookieSecret: 'cookieSecret', |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function ensurePublicDirectoriesExist() { |
|
for (const dir of Object.values(PUBLIC_DIRECTORIES)) { |
|
if (!fs.existsSync(dir)) { |
|
fs.mkdirSync(dir, { recursive: true }); |
|
} |
|
} |
|
|
|
const userHandles = await getAllUserHandles(); |
|
const directoriesList = userHandles.map(handle => getUserDirectories(handle)); |
|
for (const userDirectories of directoriesList) { |
|
for (const dir of Object.values(userDirectories)) { |
|
if (!fs.existsSync(dir)) { |
|
fs.mkdirSync(dir, { recursive: true }); |
|
} |
|
} |
|
} |
|
return directoriesList; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function logSecurityAlert(message) { |
|
const { basicAuthMode, whitelistMode } = globalThis.COMMAND_LINE_ARGS; |
|
if (basicAuthMode || whitelistMode) return; |
|
console.error(color.red(message)); |
|
if (getConfigValue('securityOverride', false, 'boolean')) { |
|
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); |
|
return; |
|
} |
|
process.exit(1); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export async function verifySecuritySettings() { |
|
const { listen, basicAuthMode } = globalThis.COMMAND_LINE_ARGS; |
|
|
|
|
|
if (!listen) { |
|
return; |
|
} |
|
|
|
if (!ENABLE_ACCOUNTS) { |
|
logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.'); |
|
} |
|
|
|
const users = await getAllEnabledUsers(); |
|
const unprotectedUsers = users.filter(x => !x.password); |
|
const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin); |
|
|
|
if (unprotectedUsers.length > 0) { |
|
console.warn(color.blue('A friendly reminder that the following users are not password protected:')); |
|
unprotectedUsers.map(x => `${color.yellow(x.handle)} ${color.red(x.admin ? '(admin)' : '')}`).forEach(x => console.warn(x)); |
|
console.log(); |
|
console.warn(`Consider setting a password in the admin panel or by using the ${color.blue('recover.js')} script.`); |
|
console.log(); |
|
|
|
if (unprotectedAdminUsers.length > 0) { |
|
logSecurityAlert('If you are not using basic authentication or whitelisting, you should set a password for all admin users.'); |
|
} |
|
} |
|
|
|
if (basicAuthMode) { |
|
const perUserBasicAuth = getConfigValue('perUserBasicAuth', false, 'boolean'); |
|
if (perUserBasicAuth && !ENABLE_ACCOUNTS) { |
|
console.error(color.red( |
|
'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.', |
|
)); |
|
} else if (!perUserBasicAuth) { |
|
const basicAuthUserName = getConfigValue('basicAuthUser.username', ''); |
|
const basicAuthUserPassword = getConfigValue('basicAuthUser.password', ''); |
|
if (!basicAuthUserName || !basicAuthUserPassword) { |
|
console.warn(color.yellow( |
|
'Basic Authentication is enabled, but username or password is not set or empty!', |
|
)); |
|
} |
|
} |
|
} |
|
} |
|
|
|
export function cleanUploads() { |
|
try { |
|
const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); |
|
if (fs.existsSync(uploadsPath)) { |
|
const uploads = fs.readdirSync(uploadsPath); |
|
|
|
if (!uploads.length) { |
|
return; |
|
} |
|
|
|
console.debug(`Cleaning uploads folder (${uploads.length} files)`); |
|
uploads.forEach(file => { |
|
const pathToFile = path.join(uploadsPath, file); |
|
fs.unlinkSync(pathToFile); |
|
}); |
|
} |
|
} catch (err) { |
|
console.error(err); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export async function getUserDirectoriesList() { |
|
const userHandles = await getAllUserHandles(); |
|
const directoriesList = userHandles.map(handle => getUserDirectories(handle)); |
|
return directoriesList; |
|
} |
|
|
|
|
|
|
|
|
|
export async function migrateUserData() { |
|
const publicDirectory = path.join(process.cwd(), 'public'); |
|
|
|
|
|
if (!fs.existsSync(path.join(publicDirectory, 'characters'))) { |
|
return; |
|
} |
|
|
|
const TIMEOUT = 10; |
|
|
|
console.log(); |
|
console.log(color.magenta('Preparing to migrate user data...')); |
|
console.log(`All public data will be moved to the ${globalThis.DATA_ROOT} directory.`); |
|
console.log('This process may take a while depending on the amount of data to move.'); |
|
console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`); |
|
console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`); |
|
|
|
for (let i = TIMEOUT; i > 0; i--) { |
|
console.log(`${i}...`); |
|
await delay(1000); |
|
} |
|
|
|
console.log(color.magenta('Starting migration... Do not interrupt the process!')); |
|
|
|
const userDirectories = getUserDirectories(DEFAULT_USER.handle); |
|
|
|
const dataMigrationMap = [ |
|
{ |
|
old: path.join(publicDirectory, 'assets'), |
|
new: userDirectories.assets, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'backgrounds'), |
|
new: userDirectories.backgrounds, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'characters'), |
|
new: userDirectories.characters, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'chats'), |
|
new: userDirectories.chats, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'context'), |
|
new: userDirectories.context, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'group chats'), |
|
new: userDirectories.groupChats, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'groups'), |
|
new: userDirectories.groups, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'instruct'), |
|
new: userDirectories.instruct, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'KoboldAI Settings'), |
|
new: userDirectories.koboldAI_Settings, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'movingUI'), |
|
new: userDirectories.movingUI, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'NovelAI Settings'), |
|
new: userDirectories.novelAI_Settings, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'OpenAI Settings'), |
|
new: userDirectories.openAI_Settings, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'QuickReplies'), |
|
new: userDirectories.quickreplies, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'TextGen Settings'), |
|
new: userDirectories.textGen_Settings, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'themes'), |
|
new: userDirectories.themes, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'user'), |
|
new: userDirectories.user, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'User Avatars'), |
|
new: userDirectories.avatars, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'worlds'), |
|
new: userDirectories.worlds, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'scripts/extensions/third-party'), |
|
new: userDirectories.extensions, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(process.cwd(), 'thumbnails'), |
|
new: userDirectories.thumbnails, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(process.cwd(), 'vectors'), |
|
new: userDirectories.vectors, |
|
file: false, |
|
}, |
|
{ |
|
old: path.join(process.cwd(), 'secrets.json'), |
|
new: path.join(userDirectories.root, 'secrets.json'), |
|
file: true, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'settings.json'), |
|
new: path.join(userDirectories.root, 'settings.json'), |
|
file: true, |
|
}, |
|
{ |
|
old: path.join(publicDirectory, 'stats.json'), |
|
new: path.join(userDirectories.root, 'stats.json'), |
|
file: true, |
|
}, |
|
]; |
|
|
|
const currentDate = new Date().toISOString().split('T')[0]; |
|
const backupDirectory = path.join(process.cwd(), PUBLIC_DIRECTORIES.backups, '_migration', currentDate); |
|
|
|
if (!fs.existsSync(backupDirectory)) { |
|
fs.mkdirSync(backupDirectory, { recursive: true }); |
|
} |
|
|
|
const errors = []; |
|
|
|
for (const migration of dataMigrationMap) { |
|
console.log(`Migrating ${migration.old} to ${migration.new}...`); |
|
|
|
try { |
|
if (!fs.existsSync(migration.old)) { |
|
console.log(color.yellow(`Skipping migration of ${migration.old} as it does not exist.`)); |
|
continue; |
|
} |
|
|
|
if (migration.file) { |
|
|
|
fs.cpSync(migration.old, migration.new, { force: true }); |
|
|
|
fs.cpSync( |
|
migration.old, |
|
path.join(backupDirectory, path.basename(migration.old)), |
|
{ recursive: true, force: true }, |
|
); |
|
fs.rmSync(migration.old, { recursive: true, force: true }); |
|
} else { |
|
|
|
fs.cpSync(migration.old, migration.new, { recursive: true, force: true }); |
|
|
|
fs.cpSync( |
|
migration.old, |
|
path.join(backupDirectory, path.basename(migration.old)), |
|
{ recursive: true, force: true }, |
|
); |
|
fs.rmSync(migration.old, { recursive: true, force: true }); |
|
} |
|
} catch (error) { |
|
console.error(color.red(`Error migrating ${migration.old} to ${migration.new}:`), error.message); |
|
errors.push(migration.old); |
|
} |
|
} |
|
|
|
if (errors.length > 0) { |
|
console.log(color.red('Migration completed with errors. Move the following files manually:')); |
|
errors.forEach(error => console.error(error)); |
|
} |
|
|
|
console.log(color.green('Migration completed!')); |
|
} |
|
|
|
export async function migrateSystemPrompts() { |
|
|
|
|
|
|
|
|
|
async function getDefaultSystemPrompts() { |
|
try { |
|
return getContentOfType('sysprompt', 'json'); |
|
} catch { |
|
return []; |
|
} |
|
} |
|
|
|
const directories = await getUserDirectoriesList(); |
|
for (const directory of directories) { |
|
try { |
|
const migrateMarker = path.join(directory.sysprompt, '.migrated'); |
|
if (fs.existsSync(migrateMarker)) { |
|
continue; |
|
} |
|
const backupsPath = path.join(directory.backups, '_sysprompt'); |
|
fs.mkdirSync(backupsPath, { recursive: true }); |
|
const defaultPrompts = await getDefaultSystemPrompts(); |
|
const instucts = fs.readdirSync(directory.instruct); |
|
let migratedPrompts = []; |
|
for (const instruct of instucts) { |
|
const instructPath = path.join(directory.instruct, instruct); |
|
const sysPromptPath = path.join(directory.sysprompt, instruct); |
|
if (path.extname(instruct) === '.json' && !fs.existsSync(sysPromptPath)) { |
|
const instructData = JSON.parse(fs.readFileSync(instructPath, 'utf8')); |
|
if ('system_prompt' in instructData && 'name' in instructData) { |
|
const backupPath = path.join(backupsPath, `${instructData.name}.json`); |
|
fs.cpSync(instructPath, backupPath, { force: true }); |
|
const syspromptData = { name: instructData.name, content: instructData.system_prompt }; |
|
migratedPrompts.push(syspromptData); |
|
delete instructData.system_prompt; |
|
writeFileAtomicSync(instructPath, JSON.stringify(instructData, null, 4)); |
|
} |
|
} |
|
} |
|
|
|
migratedPrompts = _.uniqBy(migratedPrompts, 'content'); |
|
|
|
migratedPrompts = migratedPrompts.filter(x => !defaultPrompts.some(y => y.content === x.content)); |
|
for (const sysPromptData of migratedPrompts) { |
|
sysPromptData.name = `[Migrated] ${sysPromptData.name}`; |
|
const syspromptPath = path.join(directory.sysprompt, `${sysPromptData.name}.json`); |
|
writeFileAtomicSync(syspromptPath, JSON.stringify(sysPromptData, null, 4)); |
|
console.log(`Migrated system prompt ${sysPromptData.name} for ${directory.root.split(path.sep).pop()}`); |
|
} |
|
writeFileAtomicSync(migrateMarker, ''); |
|
} catch (error) { |
|
console.error('Error migrating system prompts:', error); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function toKey(handle) { |
|
return `${KEY_PREFIX}${handle}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function toAvatarKey(handle) { |
|
return `${AVATAR_PREFIX}${handle}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function initUserStorage(dataRoot) { |
|
console.log('Using data root:', color.green(dataRoot)); |
|
await storage.init({ |
|
dir: path.join(dataRoot, '_storage'), |
|
ttl: false, |
|
}); |
|
|
|
const keys = await getAllUserHandles(); |
|
|
|
|
|
if (keys.length === 0) { |
|
await storage.setItem(toKey(DEFAULT_USER.handle), DEFAULT_USER); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getCookieSecret(dataRoot) { |
|
const cookieSecretPath = path.join(dataRoot, COOKIE_SECRET_PATH); |
|
|
|
if (fs.existsSync(cookieSecretPath)) { |
|
const stat = fs.statSync(cookieSecretPath); |
|
if (stat.size > 0) { |
|
return fs.readFileSync(cookieSecretPath, 'utf8'); |
|
} |
|
} |
|
|
|
const oldSecret = getConfigValue(STORAGE_KEYS.cookieSecret); |
|
if (oldSecret) { |
|
console.log('Migrating cookie secret from config.yaml...'); |
|
writeFileAtomicSync(cookieSecretPath, oldSecret, { encoding: 'utf8' }); |
|
return oldSecret; |
|
} |
|
|
|
console.warn(color.yellow('Cookie secret is missing from data root. Generating a new one...')); |
|
const secret = crypto.randomBytes(64).toString('base64'); |
|
writeFileAtomicSync(cookieSecretPath, secret, { encoding: 'utf8' }); |
|
return secret; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function getPasswordSalt() { |
|
return crypto.randomBytes(16).toString('base64'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function getCookieSessionName() { |
|
|
|
const hostname = os.hostname() || 'localhost'; |
|
const suffix = crypto.createHash('sha256').update(hostname).digest('hex').slice(0, 8); |
|
return `session-${suffix}`; |
|
} |
|
|
|
export function getSessionCookieAge() { |
|
|
|
const configValue = getConfigValue('sessionTimeout', -1, 'number'); |
|
|
|
|
|
if (configValue > 0) { |
|
return configValue * 1000; |
|
} |
|
|
|
|
|
if (configValue < 0) { |
|
return 400 * 24 * 60 * 60 * 1000; |
|
} |
|
|
|
|
|
|
|
return undefined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getPasswordHash(password, salt) { |
|
return crypto.scryptSync(password.normalize(), salt, 64).toString('base64'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getCsrfSecret(request) { |
|
if (!request || !request.user) { |
|
return ANON_CSRF_SECRET; |
|
} |
|
|
|
let csrfSecret = readSecret(request.user.directories, STORAGE_KEYS.csrfSecret); |
|
|
|
if (!csrfSecret) { |
|
csrfSecret = crypto.randomBytes(64).toString('base64'); |
|
writeSecret(request.user.directories, STORAGE_KEYS.csrfSecret, csrfSecret); |
|
} |
|
|
|
return csrfSecret; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export async function getAllUserHandles() { |
|
const keys = await storage.keys(x => x.key.startsWith(KEY_PREFIX)); |
|
const handles = keys.map(x => x.replace(KEY_PREFIX, '')); |
|
return handles; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getUserDirectories(handle) { |
|
if (DIRECTORIES_CACHE.has(handle)) { |
|
const cache = DIRECTORIES_CACHE.get(handle); |
|
if (cache) { |
|
return cache; |
|
} |
|
} |
|
|
|
const directories = structuredClone(USER_DIRECTORY_TEMPLATE); |
|
for (const key in directories) { |
|
directories[key] = path.join(globalThis.DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]); |
|
} |
|
DIRECTORIES_CACHE.set(handle, directories); |
|
return directories; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getUserAvatar(handle) { |
|
try { |
|
|
|
const avatarKey = toAvatarKey(handle); |
|
const avatar = await storage.getItem(avatarKey); |
|
|
|
if (avatar) { |
|
return avatar; |
|
} |
|
|
|
|
|
const directory = getUserDirectories(handle); |
|
const pathToSettings = path.join(directory.root, SETTINGS_FILE); |
|
const settings = fs.existsSync(pathToSettings) ? JSON.parse(fs.readFileSync(pathToSettings, 'utf8')) : {}; |
|
const avatarFile = settings?.power_user?.default_persona || settings?.user_avatar; |
|
if (!avatarFile) { |
|
return PUBLIC_USER_AVATAR; |
|
} |
|
const avatarPath = path.join(directory.avatars, avatarFile); |
|
if (!fs.existsSync(avatarPath)) { |
|
return PUBLIC_USER_AVATAR; |
|
} |
|
const mimeType = mime.lookup(avatarPath); |
|
const base64Content = fs.readFileSync(avatarPath, 'base64'); |
|
return `data:${mimeType};base64,${base64Content}`; |
|
} |
|
catch { |
|
|
|
return PUBLIC_USER_AVATAR; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function shouldRedirectToLogin(request) { |
|
return ENABLE_ACCOUNTS && !request.user; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function tryAutoLogin(request, basicAuthMode) { |
|
if (!ENABLE_ACCOUNTS || request.user || !request.session) { |
|
return false; |
|
} |
|
|
|
if (!request.query.noauto) { |
|
if (await singleUserLogin(request)) { |
|
return true; |
|
} |
|
|
|
if (AUTHELIA_AUTH && await autheliaUserLogin(request)) { |
|
return true; |
|
} |
|
|
|
if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function singleUserLogin(request) { |
|
if (!request.session) { |
|
return false; |
|
} |
|
|
|
const userHandles = await getAllUserHandles(); |
|
if (userHandles.length === 1) { |
|
const user = await storage.getItem(toKey(userHandles[0])); |
|
if (user && !user.password) { |
|
request.session.handle = userHandles[0]; |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function autheliaUserLogin(request) { |
|
if (!request.session) { |
|
return false; |
|
} |
|
|
|
const remoteUser = request.get('Remote-User'); |
|
if (!remoteUser) { |
|
return false; |
|
} |
|
|
|
const userHandles = await getAllUserHandles(); |
|
for (const userHandle of userHandles) { |
|
if (remoteUser === userHandle) { |
|
const user = await storage.getItem(toKey(userHandle)); |
|
if (user && user.enabled) { |
|
request.session.handle = userHandle; |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function basicUserLogin(request) { |
|
if (!request.session) { |
|
return false; |
|
} |
|
|
|
const authHeader = request.headers.authorization; |
|
|
|
if (!authHeader) { |
|
return false; |
|
} |
|
|
|
const [scheme, credentials] = authHeader.split(' '); |
|
|
|
if (scheme !== 'Basic' || !credentials) { |
|
return false; |
|
} |
|
|
|
const [username, password] = Buffer.from(credentials, 'base64') |
|
.toString('utf8') |
|
.split(':'); |
|
|
|
const userHandles = await getAllUserHandles(); |
|
for (const userHandle of userHandles) { |
|
if (username === userHandle) { |
|
const user = await storage.getItem(toKey(userHandle)); |
|
|
|
if (user && user.enabled && user.password && user.password === getPasswordHash(password, user.salt)) { |
|
request.session.handle = userHandle; |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function setUserDataMiddleware(request, response, next) { |
|
|
|
if (!ENABLE_ACCOUNTS) { |
|
const handle = DEFAULT_USER.handle; |
|
const directories = getUserDirectories(handle); |
|
request.user = { |
|
profile: DEFAULT_USER, |
|
directories: directories, |
|
}; |
|
return next(); |
|
} |
|
|
|
if (!request.session) { |
|
console.error('Session not available'); |
|
return response.sendStatus(500); |
|
} |
|
|
|
|
|
let handle = request.session?.handle; |
|
|
|
|
|
if (!handle) { |
|
return next(); |
|
} |
|
|
|
|
|
const user = await storage.getItem(toKey(handle)); |
|
|
|
if (!user) { |
|
console.error('User not found:', handle); |
|
return next(); |
|
} |
|
|
|
if (!user.enabled) { |
|
console.error('User is disabled:', handle); |
|
return next(); |
|
} |
|
|
|
const directories = getUserDirectories(handle); |
|
request.user = { |
|
profile: user, |
|
directories: directories, |
|
}; |
|
|
|
|
|
if (request.method === 'GET' && request.path === '/') { |
|
request.session.touch = Date.now(); |
|
} |
|
|
|
return next(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function requireLoginMiddleware(request, response, next) { |
|
if (!request.user) { |
|
return response.sendStatus(403); |
|
} |
|
|
|
return next(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function loginPageMiddleware(request, response) { |
|
if (!ENABLE_ACCOUNTS) { |
|
console.log('User accounts are disabled. Redirecting to index page.'); |
|
return response.redirect('/'); |
|
} |
|
|
|
try { |
|
const { basicAuthMode } = globalThis.COMMAND_LINE_ARGS; |
|
const autoLogin = await tryAutoLogin(request, basicAuthMode); |
|
|
|
if (autoLogin) { |
|
return response.redirect('/'); |
|
} |
|
} catch (error) { |
|
console.error('Error during auto-login:', error); |
|
} |
|
|
|
return response.sendFile('login.html', { root: path.join(serverDirectory, 'public') }); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function createRouteHandler(directoryFn) { |
|
return async (req, res) => { |
|
try { |
|
const directory = directoryFn(req); |
|
const filePath = decodeURIComponent(req.params[0]); |
|
const exists = fs.existsSync(path.join(directory, filePath)); |
|
if (!exists) { |
|
return res.sendStatus(404); |
|
} |
|
return res.sendFile(filePath, { root: directory }); |
|
} catch (error) { |
|
return res.sendStatus(500); |
|
} |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function createExtensionsRouteHandler(directoryFn) { |
|
return async (req, res) => { |
|
try { |
|
const directory = directoryFn(req); |
|
const filePath = decodeURIComponent(req.params[0]); |
|
|
|
const existsLocal = fs.existsSync(path.join(directory, filePath)); |
|
if (existsLocal) { |
|
return res.sendFile(filePath, { root: directory }); |
|
} |
|
|
|
const existsGlobal = fs.existsSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, filePath)); |
|
if (existsGlobal) { |
|
return res.sendFile(filePath, { root: PUBLIC_DIRECTORIES.globalExtensions }); |
|
} |
|
|
|
return res.sendStatus(404); |
|
} catch (error) { |
|
return res.sendStatus(500); |
|
} |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function requireAdminMiddleware(request, response, next) { |
|
if (!request.user) { |
|
return response.sendStatus(403); |
|
} |
|
|
|
if (request.user.profile.admin) { |
|
return next(); |
|
} |
|
|
|
console.warn('Unauthorized access to admin endpoint:', request.originalUrl); |
|
return response.sendStatus(403); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function createBackupArchive(handle, response) { |
|
const directories = getUserDirectories(handle); |
|
|
|
console.info('Backup requested for', handle); |
|
const archive = archiver('zip'); |
|
|
|
archive.on('error', function (err) { |
|
response.status(500).send({ error: err.message }); |
|
}); |
|
|
|
|
|
archive.on('end', function () { |
|
console.info('Archive wrote %d bytes', archive.pointer()); |
|
response.end(); |
|
}); |
|
|
|
const timestamp = generateTimestamp(); |
|
|
|
|
|
response.attachment(`${handle}-${timestamp}.zip`); |
|
|
|
|
|
|
|
archive.pipe(response); |
|
|
|
|
|
archive.directory(directories.root, false); |
|
archive.finalize(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function getAllUsers() { |
|
if (!ENABLE_ACCOUNTS) { |
|
return []; |
|
} |
|
|
|
|
|
|
|
const users = await storage.values(); |
|
return users; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export async function getAllEnabledUsers() { |
|
const users = await getAllUsers(); |
|
return users.filter(x => x.enabled); |
|
} |
|
|
|
|
|
|
|
|
|
export const router = express.Router(); |
|
router.use('/backgrounds/*', createRouteHandler(req => req.user.directories.backgrounds)); |
|
router.use('/characters/*', createRouteHandler(req => req.user.directories.characters)); |
|
router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.avatars)); |
|
router.use('/assets/*', createRouteHandler(req => req.user.directories.assets)); |
|
router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages)); |
|
router.use('/user/files/*', createRouteHandler(req => req.user.directories.files)); |
|
router.use('/scripts/extensions/third-party/*', createExtensionsRouteHandler(req => req.user.directories.extensions)); |
|
|