diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js
index b06ba8a8cc..59d7ec7ab9 100644
--- a/api/server/controllers/AuthController.js
+++ b/api/server/controllers/AuthController.js
@@ -84,7 +84,7 @@ const refreshController = async (req, res) => {
}
try {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
- const user = await getUserById(payload.id, '-password -__v -totpSecret');
+ const user = await getUserById(payload.id, '-password -__v -totpSecret -backupCodes');
if (!user) {
return res.status(401).redirect('/login');
}
diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js
index 7b1fc92912..9ef2718108 100644
--- a/api/server/controllers/TwoFactorController.js
+++ b/api/server/controllers/TwoFactorController.js
@@ -47,7 +47,7 @@ const verify2FA = async (req, res) => {
try {
const userId = req.user.id;
const { token, backupCode } = req.body;
- const user = await getUserById(userId);
+ const user = await getUserById(userId, '_id totpSecret backupCodes');
if (!user || !user.totpSecret) {
return res.status(400).json({ message: '2FA not initiated' });
@@ -79,7 +79,7 @@ const confirm2FA = async (req, res) => {
try {
const userId = req.user.id;
const { token } = req.body;
- const user = await getUserById(userId);
+ const user = await getUserById(userId, '_id totpSecret');
if (!user || !user.totpSecret) {
return res.status(400).json({ message: '2FA not initiated' });
@@ -105,7 +105,7 @@ const disable2FA = async (req, res) => {
try {
const userId = req.user.id;
const { token, backupCode } = req.body;
- const user = await getUserById(userId);
+ const user = await getUserById(userId, '_id totpSecret backupCodes');
if (!user || !user.totpSecret) {
return res.status(400).json({ message: '2FA is not setup for this user' });
diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js
index 9912f79ae2..cd88cb2de9 100644
--- a/api/server/controllers/UserController.js
+++ b/api/server/controllers/UserController.js
@@ -24,7 +24,13 @@ const { getMCPManager } = require('~/config');
const getUserController = async (req, res) => {
/** @type {MongoUser} */
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
+ /**
+ * These fields should not exist due to secure field selection, but deletion
+ * is done in case of alternate database incompatibility with Mongo API
+ * */
+ delete userData.password;
delete userData.totpSecret;
+ delete userData.backupCodes;
if (req.app.locals.fileStrategy === FileSources.s3 && userData.avatar) {
const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600);
if (!avatarNeedsRefresh) {
diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js
index b37c89a998..9e4af2e986 100644
--- a/api/server/controllers/auth/TwoFactorAuthController.js
+++ b/api/server/controllers/auth/TwoFactorAuthController.js
@@ -22,10 +22,11 @@ const verify2FAWithTempToken = async (req, res) => {
try {
payload = jwt.verify(tempToken, process.env.JWT_SECRET);
} catch (err) {
+ logger.error('Failed to verify temporary token:', err);
return res.status(401).json({ message: 'Invalid or expired temporary token' });
}
- const user = await getUserById(payload.userId);
+ const user = await getUserById(payload.userId, '+totpSecret +backupCodes');
if (!user || !user.twoFactorEnabled) {
return res.status(400).json({ message: '2FA is not enabled for this user' });
}
@@ -42,11 +43,11 @@ const verify2FAWithTempToken = async (req, res) => {
return res.status(401).json({ message: 'Invalid 2FA code or backup code' });
}
- // Prepare user data to return (omit sensitive fields).
const userData = user.toObject ? user.toObject() : { ...user };
- delete userData.password;
delete userData.__v;
+ delete userData.password;
delete userData.totpSecret;
+ delete userData.backupCodes;
userData.id = user._id.toString();
const authToken = await setAuthTokens(user._id, res);
diff --git a/api/strategies/jwtStrategy.js b/api/strategies/jwtStrategy.js
index 6793873ee8..386b7bafa1 100644
--- a/api/strategies/jwtStrategy.js
+++ b/api/strategies/jwtStrategy.js
@@ -12,7 +12,7 @@ const jwtLogin = () =>
},
async (payload, done) => {
try {
- const user = await getUserById(payload?.id, '-password -__v -totpSecret');
+ const user = await getUserById(payload?.id, '-password -__v -totpSecret -backupCodes');
if (user) {
user.id = user._id.toString();
if (!user.role) {
diff --git a/api/strategies/localStrategy.js b/api/strategies/localStrategy.js
index bc84e7c6b5..26b3b8197f 100644
--- a/api/strategies/localStrategy.js
+++ b/api/strategies/localStrategy.js
@@ -22,7 +22,7 @@ async function passportLogin(req, email, password, done) {
return done(null, false, { message: validationError });
}
- const user = await findUser({ email: email.trim() });
+ const user = await findUser({ email: email.trim() }, '+password');
if (!user) {
logError('Passport Local Strategy - User Not Found', { email });
logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`);
diff --git a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx
index cb05670736..fc3b545f0c 100644
--- a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx
+++ b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { RefreshCcw, ShieldX } from 'lucide-react';
+import { RefreshCcw } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider';
import {
@@ -73,8 +73,8 @@ const BackupCodesItem: React.FC = () => {
-
@@ -93,6 +93,16 @@ const BackupCodesItem: React.FC = () => {
>
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? (
<>
+
+
+ {localize('com_ui_backup_codes_security_info')}
+
+
+
+
+ {localize('com_ui_backup_codes_status')}
+
+
{user?.backupCodes.map((code, index) => {
const isUsed = code.used;
@@ -125,7 +135,7 @@ const BackupCodesItem: React.FC = () => {
>
- #{index + 1}
+ {localize('com_ui_backup_code_number', { number: index + 1 })}
{
>
) : (
-
-
{localize('com_ui_no_backup_codes')}
{
className="px-8 py-3 transition-all disabled:opacity-50"
>
{isLoading && }
- {localize('com_ui_generate_backup')}
+ {localize('com_ui_regenerate_backup')}
)}
diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts
index 2d00320c0d..298ddd9b64 100644
--- a/client/src/data-provider/Auth/mutations.ts
+++ b/client/src/data-provider/Auth/mutations.ts
@@ -87,8 +87,6 @@ export const useDeleteUserMutation = (
});
};
-// Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0
-
export const useEnableTwoFactorMutation = (): UseMutationResult<
t.TEnable2FAResponse,
unknown,
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index 0433a3cfcc..11bbae6e9f 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -633,8 +633,11 @@
"com_ui_back_to_chat": "Back to Chat",
"com_ui_back_to_prompts": "Back to Prompts",
"com_ui_backup_codes": "Backup Codes",
+ "com_ui_backup_code_number": "Code #{{number}}",
"com_ui_backup_codes_regenerate_error": "There was an error regenerating backup codes",
"com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully",
+ "com_ui_backup_codes_security_info": "For security reasons, backup codes are only displayed once when generated. Please save them in a secure location.",
+ "com_ui_backup_codes_status": "Backup Codes Status",
"com_ui_basic": "Basic",
"com_ui_basic_auth_header": "Basic authorization header",
"com_ui_bearer": "Bearer",
@@ -848,7 +851,6 @@
"com_ui_fork_split_target_setting": "Start fork from target message by default",
"com_ui_fork_success": "Successfully forked conversation",
"com_ui_fork_visible": "Visible messages only",
- "com_ui_generate_backup": "Generate Backup Codes",
"com_ui_generate_qrcode": "Generate QR Code",
"com_ui_generating": "Generating...",
"com_ui_generation_settings": "Generation Settings",
@@ -938,7 +940,6 @@
"com_ui_new_conversation_title": "New Conversation Title",
"com_ui_next": "Next",
"com_ui_no": "No",
- "com_ui_no_backup_codes": "No backup codes available. Please generate new ones",
"com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one",
"com_ui_no_categories": "No categories available",
"com_ui_no_category": "No category",
@@ -1049,7 +1050,6 @@
"com_ui_shared_link_not_found": "Shared link not found",
"com_ui_shared_prompts": "Shared Prompts",
"com_ui_shop": "Shopping",
- "com_ui_show": "Show",
"com_ui_show_all": "Show All",
"com_ui_show_image_details": "Show Image Details",
"com_ui_show_password": "Show password",
diff --git a/package-lock.json b/package-lock.json
index e0bb26f24f..312a03976a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51357,7 +51357,6 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-replace": "^5.0.5",
- "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@types/bun": "^1.2.15",
"@types/diff": "^6.0.0",
@@ -51877,7 +51876,7 @@
},
"packages/data-schemas": {
"name": "@librechat/data-schemas",
- "version": "0.0.16",
+ "version": "0.0.17",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",
diff --git a/packages/api/package.json b/packages/api/package.json
index 225b280ae8..0092071e15 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -15,12 +15,15 @@
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && rollup -c --bundleConfigAsCjs",
- "build:watch": "rollup -c -w --bundleConfigAsCjs",
+ "build:dev": "npm run clean && NODE_ENV=development rollup -c --bundleConfigAsCjs",
+ "build:watch": "NODE_ENV=development rollup -c -w --bundleConfigAsCjs",
+ "build:watch:prod": "rollup -c -w --bundleConfigAsCjs",
"test": "jest --coverage --watch",
"test:ci": "jest --coverage --ci",
"verify": "npm run test:ci",
"b:clean": "bun run rimraf dist",
"b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs",
+ "b:build:dev": "bun run b:clean && NODE_ENV=development bun run rollup -c --silent --bundleConfigAsCjs",
"start:everything-sse": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/examples/everything/sse.ts",
"start:everything": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/everything.ts",
"start:filesystem": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/filesystem.ts",
@@ -45,7 +48,6 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-replace": "^5.0.5",
- "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@types/bun": "^1.2.15",
"@types/diff": "^6.0.0",
diff --git a/packages/api/rollup.config.js b/packages/api/rollup.config.js
index 13d805874e..618927f468 100644
--- a/packages/api/rollup.config.js
+++ b/packages/api/rollup.config.js
@@ -1,7 +1,6 @@
// rollup.config.js
import { readFileSync } from 'fs';
import json from '@rollup/plugin-json';
-import terser from '@rollup/plugin-terser';
import replace from '@rollup/plugin-replace';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
@@ -10,13 +9,18 @@ import peerDepsExternal from 'rollup-plugin-peer-deps-external';
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
+/**
+ * Check if we're in development mode
+ */
+const isDevelopment = process.env.NODE_ENV === 'development';
+
const plugins = [
peerDepsExternal(),
resolve({
preferBuiltins: true,
}),
replace({
- __IS_DEV__: process.env.NODE_ENV === 'development',
+ __IS_DEV__: isDevelopment,
preventAssignment: true,
}),
commonjs({
@@ -24,12 +28,18 @@ const plugins = [
requireReturnsDefault: 'auto',
}),
typescript({
- tsconfig: './tsconfig.json',
+ tsconfig: './tsconfig.build.json',
outDir: './dist',
sourceMap: true,
- inlineSourceMap: true,
+ /**
+ * Remove inline sourcemaps - they conflict with external sourcemaps
+ */
+ inlineSourceMap: false,
+ /**
+ * Always include source content in sourcemaps for better debugging
+ */
+ inlineSources: true,
}),
- terser(),
json(),
];
@@ -41,6 +51,20 @@ const cjsBuild = {
sourcemap: true,
exports: 'named',
entryFileNames: '[name].js',
+ /**
+ * Always include sources in sourcemap for better debugging
+ */
+ sourcemapExcludeSources: false,
+ /**
+ * Use absolute paths in sourcemaps for better IDE support
+ */
+ sourcemapPathTransform: (relativeSourcePath) => {
+ /**
+ * Convert to absolute path for better debugger support
+ */
+ const path = require('path');
+ return path.resolve(relativeSourcePath);
+ },
},
external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})],
preserveSymlinks: true,
diff --git a/packages/api/tsconfig.build.json b/packages/api/tsconfig.build.json
new file mode 100644
index 0000000000..fca6775cf3
--- /dev/null
+++ b/packages/api/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "sourceMap": true,
+ "inlineSources": true,
+ "removeComments": false,
+ "preserveConstEnums": true,
+ "declarationMap": true
+ },
+ "exclude": ["node_modules", "dist", "types", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json
index 91068d2e6c..516effe552 100644
--- a/packages/data-schemas/package.json
+++ b/packages/data-schemas/package.json
@@ -1,6 +1,6 @@
{
"name": "@librechat/data-schemas",
- "version": "0.0.16",
+ "version": "0.0.17",
"description": "Mongoose schemas and models for LibreChat",
"type": "module",
"main": "dist/index.cjs",
diff --git a/packages/data-schemas/src/schema/user.ts b/packages/data-schemas/src/schema/user.ts
index d46d4f9c87..9fc8f6449c 100644
--- a/packages/data-schemas/src/schema/user.ts
+++ b/packages/data-schemas/src/schema/user.ts
@@ -119,6 +119,7 @@ const userSchema = new Schema(
},
backupCodes: {
type: [BackupCodeSchema],
+ select: false,
},
refreshToken: {
type: [SessionSchema],