mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 08:20:14 +01:00
🧹 chore: pre-release cleanup 2 (#3600)
* refactor: scrollToEnd * fix(validateConvoAccess): search conversation by ID for proper validation * feat: Add unique index for conversationId and user in convoSchema * refactor: Update font sizes 1 rem -> font-size-base in style.css * fix: Assistants map type issues * refactor: Remove obsolete scripts * fix: Update DropdownNoState component to handle both string and OptionType values * refactor: Remove config/loader.js file * fix: remove crypto.randomBytes(); refactor: Create reusable function for generating token and hash
This commit is contained in:
parent
6fead1005b
commit
1ff4841603
20 changed files with 172 additions and 637 deletions
|
|
@ -1,104 +0,0 @@
|
|||
/**
|
||||
* Install script: WIP
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const { exit } = require('process');
|
||||
const { askQuestion } = require('./helpers');
|
||||
|
||||
// If we are not in a TTY, lets exit
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log('Note: we are not in a TTY, skipping install script.');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// If we are in CI env, lets exit
|
||||
if (process.env.NODE_ENV === 'CI') {
|
||||
console.log('Note: we are in a CI environment, skipping install script.');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Save the original console.log function
|
||||
const originalConsoleWarn = console.warn;
|
||||
console.warn = () => {};
|
||||
const loader = require('./loader');
|
||||
console.warn = originalConsoleWarn;
|
||||
|
||||
const rootEnvPath = loader.resolve('.env');
|
||||
|
||||
// Skip if the env file exists
|
||||
if (fs.existsSync(rootEnvPath)) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Run the upgrade script if the legacy api/env file exists
|
||||
// Todo: remove this in a future version
|
||||
if (fs.existsSync(loader.resolve('api/.env'))) {
|
||||
console.warn('Upgrade script has yet to run, lets do that!');
|
||||
require('./upgrade');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Check the example file exists
|
||||
if (!fs.existsSync(rootEnvPath + '.example')) {
|
||||
console.red('It looks like the example env file is missing, please complete setup manually.');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Copy the example file
|
||||
fs.copyFileSync(rootEnvPath + '.example', rootEnvPath);
|
||||
|
||||
// Update the secure keys!
|
||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_KEY', 32);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_IV', 16);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'MEILI_MASTER_KEY', 32);
|
||||
|
||||
// Init env
|
||||
let env = {};
|
||||
|
||||
(async () => {
|
||||
// Lets colour the console
|
||||
console.purple('=== LibreChat First Install ===');
|
||||
console.blue('Note: Leave blank to use the default value.');
|
||||
console.log(''); // New line
|
||||
|
||||
// Ask for the app title
|
||||
const title = await askQuestion('Enter the app title (default: "LibreChat"): ');
|
||||
env['APP_TITLE'] = title || 'LibreChat';
|
||||
|
||||
// Ask for OPENAI_API_KEY
|
||||
const key = await askQuestion('Enter your OPENAI_API_KEY (default: "user_provided"): ');
|
||||
env['OPENAI_API_KEY'] = key || 'user_provided';
|
||||
|
||||
// Ask about mongodb
|
||||
const mongodb = await askQuestion(
|
||||
'What is your mongodb url? (default: mongodb://127.0.0.1:27018/LibreChat)',
|
||||
);
|
||||
env['MONGO_URI'] = mongodb || 'mongodb://127.0.0.1:27018/LibreChat';
|
||||
// Very basic check to make sure they entered a url
|
||||
if (!env['MONGO_URI'].includes('://')) {
|
||||
console.orange(
|
||||
'Warning: Your mongodb url looks incorrect, please double check it in the `.env` file.',
|
||||
);
|
||||
}
|
||||
|
||||
// Lets ask about open registration
|
||||
const openReg = await askQuestion('Do you want to allow user registration (y/n)? Default: y');
|
||||
if (openReg === 'n' || openReg === 'no') {
|
||||
env['ALLOW_REGISTRATION'] = 'false';
|
||||
// Lets tell them about how to create an account:
|
||||
console.red(
|
||||
'Note: You can create an account by running: `npm run create-user <email> <name> <username>`',
|
||||
);
|
||||
// sleep for 1 second so they can read this
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// Update the env file
|
||||
loader.writeEnvFile(rootEnvPath, env);
|
||||
|
||||
// We can ask for more here if we want
|
||||
console.log(''); // New line
|
||||
console.green('Success! Please read our docs if you need help setting up the rest of the app.');
|
||||
console.log(''); // New line
|
||||
})();
|
||||
252
config/loader.js
252
config/loader.js
|
|
@ -1,252 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
/**
|
||||
* This class is responsible for loading the environment variables
|
||||
*
|
||||
* Inspired by: https://thekenyandev.com/blog/environment-variables-strategy-for-node/
|
||||
*/
|
||||
class Env {
|
||||
constructor() {
|
||||
this.envMap = {
|
||||
default: '.env',
|
||||
development: '.env.development',
|
||||
test: '.env.test',
|
||||
production: '.env.production',
|
||||
};
|
||||
|
||||
this.init();
|
||||
|
||||
this.isProduction = process.env.NODE_ENV === 'production';
|
||||
this.domains = {
|
||||
client: process.env.DOMAIN_CLIENT,
|
||||
server: process.env.DOMAIN_SERVER,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the environment variables
|
||||
*/
|
||||
init() {
|
||||
let hasDefault = false;
|
||||
// Load the default env file if it exists
|
||||
if (fs.existsSync(this.envMap.default)) {
|
||||
hasDefault = true;
|
||||
dotenv.config({
|
||||
// path: this.resolve(this.envMap.default),
|
||||
path: path.resolve(__dirname, '..', this.envMap.default),
|
||||
});
|
||||
} else {
|
||||
console.warn('The default .env file was not found');
|
||||
}
|
||||
|
||||
const environment = this.currentEnvironment();
|
||||
|
||||
// Load the environment specific env file
|
||||
const envFile = this.envMap[environment];
|
||||
|
||||
// check if the file exists
|
||||
if (fs.existsSync(envFile)) {
|
||||
dotenv.config({
|
||||
// path: this.resolve(envFile),
|
||||
path: path.resolve(__dirname, '..', envFile),
|
||||
});
|
||||
} else if (!hasDefault) {
|
||||
console.warn('No env files found, have you completed the install process?');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Config
|
||||
*/
|
||||
validate() {
|
||||
const requiredKeys = [
|
||||
'NODE_ENV',
|
||||
'JWT_SECRET',
|
||||
'DOMAIN_CLIENT',
|
||||
'DOMAIN_SERVER',
|
||||
'CREDS_KEY',
|
||||
'CREDS_IV',
|
||||
];
|
||||
|
||||
const missingKeys = requiredKeys
|
||||
.map((key) => {
|
||||
const variable = process.env[key];
|
||||
if (variable === undefined || variable === null) {
|
||||
return key;
|
||||
}
|
||||
})
|
||||
.filter((value) => value !== undefined);
|
||||
|
||||
// Throw an error if any required keys are missing
|
||||
if (missingKeys.length) {
|
||||
const message = `
|
||||
The following required env variables are missing:
|
||||
${missingKeys.toString()}.
|
||||
Please add them to your env file or run 'npm run install'
|
||||
`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Check JWT secret for default
|
||||
if (process.env.JWT_SECRET === 'secret') {
|
||||
console.warn('Warning: JWT_SECRET is set to default value');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the location of the env file
|
||||
*
|
||||
* @param {String} envFile
|
||||
* @returns
|
||||
*/
|
||||
resolve(envFile) {
|
||||
return path.resolve(process.cwd(), envFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add secure keys to the env
|
||||
*
|
||||
* @param {String} filePath The path of the .env you are updating
|
||||
* @param {String} key The env you are adding
|
||||
* @param {Number} length The length of the secure key
|
||||
*/
|
||||
addSecureEnvVar(filePath, key, length) {
|
||||
const env = {};
|
||||
env[key] = this.generateSecureRandomString(length);
|
||||
this.writeEnvFile(filePath, env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the change to the env file
|
||||
*/
|
||||
writeEnvFile(filePath, env) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const updatedLines = lines
|
||||
.map((line) => {
|
||||
if (line.trim().startsWith('#')) {
|
||||
// Allow comment removal
|
||||
if (env[line] === 'remove') {
|
||||
return null; // Mark the line for removal
|
||||
}
|
||||
// Preserve comments
|
||||
return line;
|
||||
}
|
||||
|
||||
const [key, value] = line.split('=');
|
||||
if (key && value && Object.prototype.hasOwnProperty.call(env, key.trim())) {
|
||||
if (env[key.trim()] === 'remove') {
|
||||
return null; // Mark the line for removal
|
||||
}
|
||||
return `${key.trim()}=${env[key.trim()]}`;
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.filter((line) => line !== null); // Remove lines marked for removal
|
||||
|
||||
// Add any new environment variables that are not in the file yet
|
||||
Object.entries(env).forEach(([key, value]) => {
|
||||
if (value !== 'remove' && !updatedLines.some((line) => line.startsWith(`${key}=`))) {
|
||||
updatedLines.push(`${key}=${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Loop through updatedLines and wrap values with spaces in double quotes
|
||||
const fixedLines = updatedLines.map((line) => {
|
||||
// lets only split the first = sign
|
||||
const [key, value] = line.split(/=(.+)/);
|
||||
if (typeof value === 'undefined' || line.trim().startsWith('#')) {
|
||||
return line;
|
||||
}
|
||||
// Skip lines with quotes and numbers already
|
||||
// Todo: this could be one regex
|
||||
const wrappedValue =
|
||||
value.includes(' ') && !value.includes('"') && !value.includes('\'') && !/\d/.test(value)
|
||||
? `"${value}"`
|
||||
: value;
|
||||
return `${key}=${wrappedValue}`;
|
||||
});
|
||||
|
||||
const updatedContent = fixedLines.join('\n');
|
||||
fs.writeFileSync(filePath, updatedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Secure Random Strings
|
||||
*
|
||||
* @param {Number} length The length of the random string
|
||||
* @returns
|
||||
*/
|
||||
generateSecureRandomString(length = 32) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the environment variables
|
||||
*/
|
||||
all() {
|
||||
return process.env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment variable
|
||||
*
|
||||
* @param {String} variable
|
||||
* @returns
|
||||
*/
|
||||
get(variable) {
|
||||
return process.env[variable];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current environment name
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
currentEnvironment() {
|
||||
return this.get('NODE_ENV');
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we running in development?
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isDevelopment() {
|
||||
return this.currentEnvironment() === 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we running tests?
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isTest() {
|
||||
return this.currentEnvironment() === 'test';
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we running in production?
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isProduction() {
|
||||
return this.currentEnvironment() === 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we running in CI?
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isCI() {
|
||||
return this.currentEnvironment() === 'CI';
|
||||
}
|
||||
}
|
||||
|
||||
const env = new Env();
|
||||
|
||||
module.exports = env;
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
/**
|
||||
* Upgrade script
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const dotenv = require('dotenv');
|
||||
const { exit } = require('process');
|
||||
|
||||
// Suppress default warnings
|
||||
const originalConsoleWarn = console.warn;
|
||||
console.warn = () => {};
|
||||
const loader = require('./loader');
|
||||
console.warn = originalConsoleWarn;
|
||||
|
||||
// Old Paths
|
||||
const apiEnvPath = loader.resolve('api/.env');
|
||||
const clientEnvPath = loader.resolve('client/.env');
|
||||
|
||||
// Load into env
|
||||
dotenv.config({
|
||||
path: loader.resolve(apiEnvPath),
|
||||
});
|
||||
dotenv.config({
|
||||
path: loader.resolve(clientEnvPath),
|
||||
});
|
||||
// JS was doing spooky actions at a distance, lets prevent that
|
||||
const initEnv = JSON.parse(JSON.stringify(process.env));
|
||||
|
||||
// New Paths
|
||||
const rootEnvPath = loader.resolve('.env');
|
||||
const devEnvPath = loader.resolve('.env.development');
|
||||
const prodEnvPath = loader.resolve('.env.production');
|
||||
|
||||
if (fs.existsSync(rootEnvPath)) {
|
||||
console.error('Root env file already exists! Aborting');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Validate old configs
|
||||
if (!fs.existsSync(apiEnvPath)) {
|
||||
console.error('Api env doesn\'t exit! Did you mean to run install?');
|
||||
exit(1);
|
||||
}
|
||||
if (!fs.existsSync(clientEnvPath)) {
|
||||
console.error('Client env doesn\'t exit! But api/.env does. Manual upgrade required');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refactor the ENV if it has a prod_/dev_ version
|
||||
*
|
||||
* @param {*} varDev
|
||||
* @param {*} varProd
|
||||
* @param {*} varName
|
||||
*/
|
||||
function refactorPairedEnvVar(varDev, varProd, varName) {
|
||||
// Lets validate if either of these are undefined, if so lets use the non-undefined one
|
||||
if (initEnv[varDev] === undefined && initEnv[varProd] === undefined) {
|
||||
console.error(`Both ${varDev} and ${varProd} are undefined! Manual intervention required!`);
|
||||
} else if (initEnv[varDev] === undefined) {
|
||||
fs.appendFileSync(rootEnvPath, `\n${varName}=${initEnv[varProd]}`);
|
||||
} else if (initEnv[varProd] === undefined) {
|
||||
fs.appendFileSync(rootEnvPath, `\n${varName}=${initEnv[varDev]}`);
|
||||
} else if (initEnv[varDev] === initEnv[varProd]) {
|
||||
fs.appendFileSync(rootEnvPath, `\n${varName}=${initEnv[varDev]}`);
|
||||
} else {
|
||||
fs.appendFileSync(rootEnvPath, `${varName}=${initEnv[varProd]}\n`);
|
||||
fs.appendFileSync(devEnvPath, `${varName}=${initEnv[varDev]}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the env files!
|
||||
* 1. /api/.env will merge into /.env
|
||||
* 2. /client/.env will merge into /.env
|
||||
* 3. Any prod_/dev_ keys will be split up into .env.development / .env.production files (if they are different)
|
||||
*/
|
||||
if (fs.existsSync(apiEnvPath)) {
|
||||
fs.copyFileSync(apiEnvPath, rootEnvPath);
|
||||
fs.copyFileSync(apiEnvPath, rootEnvPath + '.api.bak');
|
||||
fs.unlinkSync(apiEnvPath);
|
||||
}
|
||||
|
||||
// Clean up Domain variables
|
||||
fs.appendFileSync(
|
||||
rootEnvPath,
|
||||
'\n\n##########################\n# Domain Variables:\n# Note: DOMAIN_ vars are passed to vite\n##########################\n',
|
||||
);
|
||||
refactorPairedEnvVar('CLIENT_URL_DEV', 'CLIENT_URL_PROD', 'DOMAIN_CLIENT');
|
||||
refactorPairedEnvVar('SERVER_URL_DEV', 'SERVER_URL_PROD', 'DOMAIN_SERVER');
|
||||
|
||||
// Remove the old vars
|
||||
const removeEnvs = {
|
||||
NODE_ENV: 'remove',
|
||||
OPENAI_KEY: 'remove',
|
||||
CLIENT_URL_DEV: 'remove',
|
||||
CLIENT_URL_PROD: 'remove',
|
||||
SERVER_URL_DEV: 'remove',
|
||||
SERVER_URL_PROD: 'remove',
|
||||
JWT_SECRET_DEV: 'remove', // Lets regen
|
||||
JWT_SECRET_PROD: 'remove', // Lets regen
|
||||
VITE_APP_TITLE: 'remove',
|
||||
// Comments to remove:
|
||||
'#JWT:': 'remove',
|
||||
'# Add a secure secret for production if deploying to live domain.': 'remove',
|
||||
'# Site URLs:': 'remove',
|
||||
'# Don\'t forget to set Node env to development in the Server configuration section above':
|
||||
'remove',
|
||||
'# if you want to run in dev mode': 'remove',
|
||||
'# Change these values to domain if deploying:': 'remove',
|
||||
'# Set Node env to development if running in dev mode.': 'remove',
|
||||
};
|
||||
loader.writeEnvFile(rootEnvPath, removeEnvs);
|
||||
|
||||
/**
|
||||
* Lets make things more secure!
|
||||
* 1. Add CREDS_KEY
|
||||
* 2. Add CREDS_IV
|
||||
* 3. Add JWT_SECRET
|
||||
*/
|
||||
fs.appendFileSync(
|
||||
rootEnvPath,
|
||||
'\n\n##########################\n# Secure Keys:\n##########################\n',
|
||||
);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_KEY', 32);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_IV', 16);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32);
|
||||
|
||||
// Lets update the openai key name, not the best spot in the env file but who cares ¯\_(ツ)_/¯
|
||||
loader.writeEnvFile(rootEnvPath, { OPENAI_API_KEY: initEnv['OPENAI_KEY'] });
|
||||
|
||||
// TODO: we need to copy over the value of: APP_TITLE
|
||||
fs.appendFileSync(
|
||||
rootEnvPath,
|
||||
'\n\n##########################\n# Frontend Vite Variables:\n##########################\n',
|
||||
);
|
||||
const frontend = {
|
||||
APP_TITLE: initEnv['VITE_APP_TITLE'] || '"LibreChat"',
|
||||
ALLOW_REGISTRATION: 'true',
|
||||
};
|
||||
loader.writeEnvFile(rootEnvPath, frontend);
|
||||
|
||||
// Ensure .env.development and .env.production files end with a newline
|
||||
if (fs.existsSync(devEnvPath)) {
|
||||
fs.appendFileSync(devEnvPath, '\n');
|
||||
}
|
||||
if (fs.existsSync(prodEnvPath)) {
|
||||
fs.appendFileSync(prodEnvPath, '\n');
|
||||
}
|
||||
// Remove client file
|
||||
fs.copyFileSync(clientEnvPath, rootEnvPath + '.client.bak');
|
||||
fs.unlinkSync(clientEnvPath);
|
||||
|
||||
console.log('###############################################');
|
||||
console.log('Upgrade completed! Please review the new .env file and make any changes as needed.');
|
||||
console.log('###############################################');
|
||||
|
||||
// if the .env.development file exists, lets tell the user
|
||||
if (fs.existsSync(devEnvPath)) {
|
||||
console.log(
|
||||
'NOTE: A .env.development file was created. This will take precedence over the .env file when running in dev mode.',
|
||||
);
|
||||
console.log('###############################################');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue