| |
| 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 sanitize from 'sanitize-filename'; |
|
|
| import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; |
| import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache } 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('sso.autheliaAuth', false, 'boolean'); |
| const AUTHENTIK_AUTH = getConfigValue('sso.authentikAuth', 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 TavernIntern 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, |
| expiredInterval: 0, |
| }); |
|
|
| 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, sanitize(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 (AUTHENTIK_AUTH && await authentikUserLogin(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) { |
| return headerUserLogin(request, 'Remote-User'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function authentikUserLogin(request) { |
| return headerUserLogin(request, 'X-Authentik-Username'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function headerUserLogin(request, header = 'Remote-User') { |
| if (!request.session) { |
| return false; |
| } |
|
|
| const remoteUser = request.get(header); |
| if (!remoteUser) { |
| return false; |
| } |
| console.debug(`Attempting auto-login for user from header ${header}: ${remoteUser}`); |
|
|
| const userHandles = await getAllUserHandles(); |
| for (const userHandle of userHandles) { |
| if (remoteUser.toLowerCase() === 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); |
| } |
|
|
| invalidateFirefoxCache(filePath, req, res); |
| 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)); |
|
|