From a2fc7d312a8041dda9dae4f9e8d4bc3daf8de4f3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 30 May 2025 22:18:13 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20refactor:=20Extract=20D?= =?UTF-8?q?B=20layers=20to=20`data-schemas`=20for=20shared=20use=20(#7650)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: move model definitions and database-related methods to packages/data-schemas * ci: update tests due to new DB structure fix: disable mocking `librechat-data-provider` feat: Add schema exports to data-schemas package - Introduced a new schema module that exports various schemas including action, agent, and user schemas. - Updated index.ts to include the new schema exports for better modularity and organization. ci: fix appleStrategy tests fix: Agent.spec.js ci: refactor handleTools tests to use MongoMemoryServer for in-memory database fix: getLogStores imports ci: update banViolation tests to use MongoMemoryServer and improve session mocking test: refactor samlStrategy tests to improve mock configurations and user handling ci: fix crypto mock in handleText tests for improved accuracy ci: refactor spendTokens tests to improve model imports and setup ci: refactor Message model tests to use MongoMemoryServer and improve database interactions * refactor: streamline IMessage interface and move feedback properties to types/message.ts * refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen) * refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity * refactor: remove unused mongoose imports from model files for cleaner code * refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code * refactor: remove unused import in Transaction model for cleaner code * ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch * chore: cleanup imports --- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/dev-branch-images.yml | 72 ++++ api/app/clients/specs/BaseClient.test.js | 2 +- api/app/clients/specs/OpenAIClient.test.js | 2 +- api/app/clients/specs/PluginsClient.test.js | 2 +- .../clients/tools/util/handleTools.test.js | 52 ++- api/cache/banViolation.js | 4 +- api/cache/banViolation.spec.js | 60 +-- api/cache/getLogStores.js | 2 +- api/{lib/db/connectDb.js => db/connect.js} | 5 +- api/db/index.js | 8 + api/{lib => }/db/indexSync.js | 10 +- api/db/models.js | 5 + api/lib/db/index.js | 4 - api/models/Action.js | 5 +- api/models/Agent.js | 8 +- api/models/Agent.spec.js | 13 +- api/models/Assistant.js | 5 +- api/models/Balance.js | 4 - api/models/Banner.js | 9 +- api/models/Config.js | 86 ----- api/models/Conversation.js | 7 +- api/models/ConversationTag.js | 21 +- api/models/File.js | 8 +- api/models/Key.js | 4 - api/models/Message.js | 7 +- api/models/Message.spec.js | 307 +++++++++------ api/models/Preset.js | 5 +- api/models/Project.js | 5 +- api/models/Prompt.js | 8 +- api/models/Role.js | 40 +- api/models/Role.spec.js | 4 +- api/models/Session.js | 275 ------------- api/models/Share.js | 9 +- api/models/Token.js | 159 +------- api/models/ToolCall.js | 4 +- api/models/Transaction.js | 121 +++--- api/models/Transaction.spec.js | 7 +- api/models/User.js | 6 - api/models/balanceMethods.js | 8 +- api/models/convoStructure.spec.js | 4 +- api/models/index.js | 53 +-- api/models/inviteUser.js | 6 +- api/models/schema/convoSchema.js | 18 - api/models/schema/messageSchema.js | 16 - api/models/schema/pluginAuthSchema.js | 6 - api/models/schema/presetSchema.js | 6 - api/models/spendTokens.js | 11 +- api/models/spendTokens.spec.js | 19 +- api/models/userMethods.js | 162 -------- api/server/controllers/AuthController.js | 11 +- api/server/controllers/Balance.js | 2 +- api/server/controllers/TwoFactorController.js | 10 +- api/server/controllers/UserController.js | 7 +- .../auth/TwoFactorAuthController.js | 6 +- api/server/index.js | 4 +- api/server/middleware/checkBan.js | 2 +- api/server/middleware/checkInviteUser.js | 2 +- api/server/middleware/setBalanceConfig.js | 4 +- api/server/routes/messages.js | 10 +- api/server/services/ActionService.js | 2 +- .../services/AppService.interface.spec.js | 4 +- api/server/services/AppService.js | 2 +- api/server/services/AppService.spec.js | 4 +- api/server/services/AuthService.js | 22 +- api/server/services/Files/Azure/images.js | 5 +- api/server/services/Files/Firebase/images.js | 5 +- api/server/services/Files/Local/images.js | 3 +- api/server/services/Files/S3/images.js | 5 +- api/server/services/PluginService.js | 4 +- api/server/services/UserService.js | 7 +- api/server/services/signPayload.js | 26 -- api/server/services/twoFactorService.js | 5 +- api/server/utils/crypto.js | 7 - api/server/utils/handleText.spec.js | 10 +- api/strategies/appleStrategy.js | 8 +- api/strategies/appleStrategy.test.js | 56 +-- api/strategies/jwtStrategy.js | 2 +- api/strategies/ldapStrategy.js | 9 +- api/strategies/localStrategy.js | 4 +- api/strategies/openidStrategy.js | 19 +- api/strategies/openidStrategy.spec.js | 18 +- api/strategies/process.js | 6 +- api/strategies/samlStrategy.js | 9 +- api/strategies/samlStrategy.spec.js | 112 +++--- api/strategies/socialLogin.js | 2 +- config/add-balance.js | 7 +- config/ban-user.js | 3 +- config/connect.js | 2 +- config/create-user.js | 3 +- config/delete-user.js | 3 +- config/invite-user.js | 3 +- config/list-balances.js | 4 +- config/list-users.js | 3 +- config/reset-terms.js | 5 +- config/set-balance.js | 4 +- config/update-banner.js | 3 +- config/user-stats.js | 5 +- e2e/setup/cleanupUser.ts | 35 +- package-lock.json | 362 +++++++++++------- packages/data-schemas/package.json | 15 +- .../data-schemas/src/config/meiliLogger.ts | 75 ++++ packages/data-schemas/src/config/parsers.ts | 241 ++++++++++++ packages/data-schemas/src/config/winston.ts | 123 ++++++ packages/data-schemas/src/crypto/index.ts | 17 + packages/data-schemas/src/index.ts | 75 +--- packages/data-schemas/src/methods/index.ts | 18 + packages/data-schemas/src/methods/role.ts | 50 +++ packages/data-schemas/src/methods/session.ts | 264 +++++++++++++ packages/data-schemas/src/methods/token.ts | 105 +++++ packages/data-schemas/src/methods/user.ts | 174 +++++++++ packages/data-schemas/src/models/action.ts | 9 + packages/data-schemas/src/models/agent.ts | 9 + packages/data-schemas/src/models/assistant.ts | 9 + packages/data-schemas/src/models/balance.ts | 9 + packages/data-schemas/src/models/banner.ts | 9 + .../src/models/conversationTag.ts | 11 + packages/data-schemas/src/models/convo.ts | 11 + packages/data-schemas/src/models/file.ts | 9 + packages/data-schemas/src/models/index.ts | 52 +++ packages/data-schemas/src/models/key.ts | 8 + packages/data-schemas/src/models/message.ts | 9 + .../data-schemas/src/models/pluginAuth.ts | 8 + .../src/models/plugins/mongoMeili.ts | 358 +++++++++-------- packages/data-schemas/src/models/preset.ts | 8 + packages/data-schemas/src/models/project.ts | 8 + packages/data-schemas/src/models/prompt.ts | 8 + .../data-schemas/src/models/promptGroup.ts | 11 + packages/data-schemas/src/models/role.ts | 9 + packages/data-schemas/src/models/session.ts | 9 + .../data-schemas/src/models/sharedLink.ts | 8 + packages/data-schemas/src/models/token.ts | 9 + packages/data-schemas/src/models/toolCall.ts | 8 + .../data-schemas/src/models/transaction.ts | 10 + packages/data-schemas/src/models/user.ts | 9 + packages/data-schemas/src/schema/action.ts | 30 +- packages/data-schemas/src/schema/agent.ts | 32 +- packages/data-schemas/src/schema/assistant.ts | 17 +- packages/data-schemas/src/schema/balance.ts | 16 +- packages/data-schemas/src/schema/convo.ts | 70 +--- packages/data-schemas/src/schema/file.ts | 30 +- packages/data-schemas/src/schema/index.ts | 23 ++ packages/data-schemas/src/schema/message.ts | 56 +-- packages/data-schemas/src/schema/role.ts | 34 +- packages/data-schemas/src/schema/session.ts | 9 +- packages/data-schemas/src/schema/token.ts | 14 +- packages/data-schemas/src/schema/user.ts | 41 +- packages/data-schemas/src/types/action.ts | 28 ++ packages/data-schemas/src/types/agent.ts | 31 ++ packages/data-schemas/src/types/assistant.ts | 15 + packages/data-schemas/src/types/balance.ts | 12 + packages/data-schemas/src/types/banner.ts | 10 + packages/data-schemas/src/types/convo.ts | 53 +++ packages/data-schemas/src/types/file.ts | 27 ++ packages/data-schemas/src/types/index.ts | 12 + packages/data-schemas/src/types/message.ts | 44 +++ packages/data-schemas/src/types/role.ts | 33 ++ packages/data-schemas/src/types/session.ts | 42 ++ packages/data-schemas/src/types/token.ts | 42 ++ packages/data-schemas/src/types/user.ts | 72 ++++ packages/data-schemas/tsconfig.json | 6 +- 161 files changed, 2998 insertions(+), 2088 deletions(-) create mode 100644 .github/workflows/dev-branch-images.yml rename api/{lib/db/connectDb.js => db/connect.js} (96%) create mode 100644 api/db/index.js rename api/{lib => }/db/indexSync.js (93%) create mode 100644 api/db/models.js delete mode 100644 api/lib/db/index.js delete mode 100644 api/models/Balance.js delete mode 100644 api/models/Config.js delete mode 100644 api/models/Key.js delete mode 100644 api/models/Session.js delete mode 100644 api/models/User.js delete mode 100644 api/models/schema/convoSchema.js delete mode 100644 api/models/schema/messageSchema.js delete mode 100644 api/models/schema/pluginAuthSchema.js delete mode 100644 api/models/schema/presetSchema.js delete mode 100644 api/server/services/signPayload.js create mode 100644 packages/data-schemas/src/config/meiliLogger.ts create mode 100644 packages/data-schemas/src/config/parsers.ts create mode 100644 packages/data-schemas/src/config/winston.ts create mode 100644 packages/data-schemas/src/crypto/index.ts create mode 100644 packages/data-schemas/src/methods/index.ts create mode 100644 packages/data-schemas/src/methods/role.ts create mode 100644 packages/data-schemas/src/methods/session.ts create mode 100644 packages/data-schemas/src/methods/token.ts create mode 100644 packages/data-schemas/src/methods/user.ts create mode 100644 packages/data-schemas/src/models/action.ts create mode 100644 packages/data-schemas/src/models/agent.ts create mode 100644 packages/data-schemas/src/models/assistant.ts create mode 100644 packages/data-schemas/src/models/balance.ts create mode 100644 packages/data-schemas/src/models/banner.ts create mode 100644 packages/data-schemas/src/models/conversationTag.ts create mode 100644 packages/data-schemas/src/models/convo.ts create mode 100644 packages/data-schemas/src/models/file.ts create mode 100644 packages/data-schemas/src/models/index.ts create mode 100644 packages/data-schemas/src/models/key.ts create mode 100644 packages/data-schemas/src/models/message.ts create mode 100644 packages/data-schemas/src/models/pluginAuth.ts rename api/models/plugins/mongoMeili.js => packages/data-schemas/src/models/plugins/mongoMeili.ts (51%) create mode 100644 packages/data-schemas/src/models/preset.ts create mode 100644 packages/data-schemas/src/models/project.ts create mode 100644 packages/data-schemas/src/models/prompt.ts create mode 100644 packages/data-schemas/src/models/promptGroup.ts create mode 100644 packages/data-schemas/src/models/role.ts create mode 100644 packages/data-schemas/src/models/session.ts create mode 100644 packages/data-schemas/src/models/sharedLink.ts create mode 100644 packages/data-schemas/src/models/token.ts create mode 100644 packages/data-schemas/src/models/toolCall.ts create mode 100644 packages/data-schemas/src/models/transaction.ts create mode 100644 packages/data-schemas/src/models/user.ts create mode 100644 packages/data-schemas/src/schema/index.ts create mode 100644 packages/data-schemas/src/types/action.ts create mode 100644 packages/data-schemas/src/types/agent.ts create mode 100644 packages/data-schemas/src/types/assistant.ts create mode 100644 packages/data-schemas/src/types/balance.ts create mode 100644 packages/data-schemas/src/types/banner.ts create mode 100644 packages/data-schemas/src/types/convo.ts create mode 100644 packages/data-schemas/src/types/file.ts create mode 100644 packages/data-schemas/src/types/index.ts create mode 100644 packages/data-schemas/src/types/message.ts create mode 100644 packages/data-schemas/src/types/role.ts create mode 100644 packages/data-schemas/src/types/session.ts create mode 100644 packages/data-schemas/src/types/token.ts create mode 100644 packages/data-schemas/src/types/user.ts diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index fc1c02db6..520808439 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -2,7 +2,7 @@ name: Update Test Server on: workflow_run: - workflows: ["Docker Dev Images Build"] + workflows: ["Docker Dev Branch Images Build"] types: - completed workflow_dispatch: diff --git a/.github/workflows/dev-branch-images.yml b/.github/workflows/dev-branch-images.yml new file mode 100644 index 000000000..b7ad47031 --- /dev/null +++ b/.github/workflows/dev-branch-images.yml @@ -0,0 +1,72 @@ +name: Docker Dev Branch Images Build + +on: + workflow_dispatch: + push: + branches: + - dev + paths: + - 'api/**' + - 'client/**' + - 'packages/**' + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - target: api-build + file: Dockerfile.multi + image_name: lc-dev-api + - target: node + file: Dockerfile + image_name: lc-dev + + steps: + # Check out the repository + - name: Checkout + uses: actions/checkout@v4 + + # Set up QEMU + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Set up Docker Buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Log in to GitHub Container Registry + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Login to Docker Hub + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Prepare the environment + - name: Prepare environment + run: | + cp .env.example .env + + # Build and push Docker images for each target + - name: Build and push Docker images + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ matrix.file }} + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }} + ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest + ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }} + ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest + platforms: linux/amd64,linux/arm64 + target: ${{ matrix.target }} \ No newline at end of file diff --git a/api/app/clients/specs/BaseClient.test.js b/api/app/clients/specs/BaseClient.test.js index 1cd4a80db..0ba77db6f 100644 --- a/api/app/clients/specs/BaseClient.test.js +++ b/api/app/clients/specs/BaseClient.test.js @@ -1,7 +1,7 @@ const { Constants } = require('librechat-data-provider'); const { initializeFakeClient } = require('./FakeClient'); -jest.mock('~/lib/db/connectDb'); +jest.mock('~/db/connect'); jest.mock('~/models', () => ({ User: jest.fn(), Key: jest.fn(), diff --git a/api/app/clients/specs/OpenAIClient.test.js b/api/app/clients/specs/OpenAIClient.test.js index 8e470727d..cc4aa84d5 100644 --- a/api/app/clients/specs/OpenAIClient.test.js +++ b/api/app/clients/specs/OpenAIClient.test.js @@ -5,7 +5,7 @@ const getLogStores = require('~/cache/getLogStores'); const OpenAIClient = require('../OpenAIClient'); jest.mock('meilisearch'); -jest.mock('~/lib/db/connectDb'); +jest.mock('~/db/connect'); jest.mock('~/models', () => ({ User: jest.fn(), Key: jest.fn(), diff --git a/api/app/clients/specs/PluginsClient.test.js b/api/app/clients/specs/PluginsClient.test.js index fd7bee504..4928acefd 100644 --- a/api/app/clients/specs/PluginsClient.test.js +++ b/api/app/clients/specs/PluginsClient.test.js @@ -3,7 +3,7 @@ const { Constants } = require('librechat-data-provider'); const { HumanMessage, AIMessage } = require('@langchain/core/messages'); const PluginsClient = require('../PluginsClient'); -jest.mock('~/lib/db/connectDb'); +jest.mock('~/db/connect'); jest.mock('~/models/Conversation', () => { return function () { return { diff --git a/api/app/clients/tools/util/handleTools.test.js b/api/app/clients/tools/util/handleTools.test.js index 6538ce9aa..1cacda815 100644 --- a/api/app/clients/tools/util/handleTools.test.js +++ b/api/app/clients/tools/util/handleTools.test.js @@ -1,8 +1,5 @@ -const mockUser = { - _id: 'fakeId', - save: jest.fn(), - findByIdAndDelete: jest.fn(), -}; +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); const mockPluginService = { updateUserPluginAuth: jest.fn(), @@ -10,23 +7,18 @@ const mockPluginService = { getUserPluginAuthValue: jest.fn(), }; -jest.mock('~/models/User', () => { - return function () { - return mockUser; - }; -}); - jest.mock('~/server/services/PluginService', () => mockPluginService); const { BaseLLM } = require('@langchain/openai'); const { Calculator } = require('@langchain/community/tools/calculator'); -const User = require('~/models/User'); +const { User } = require('~/db/models'); const PluginService = require('~/server/services/PluginService'); const { validateTools, loadTools, loadToolWithAuth } = require('./handleTools'); const { StructuredSD, availableTools, DALLE3 } = require('../'); describe('Tool Handlers', () => { + let mongoServer; let fakeUser; const pluginKey = 'dalle'; const pluginKey2 = 'wolfram'; @@ -37,7 +29,9 @@ describe('Tool Handlers', () => { const authConfigs = mainPlugin.authConfig; beforeAll(async () => { - mockUser.save.mockResolvedValue(undefined); + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); const userAuthValues = {}; mockPluginService.getUserPluginAuthValue.mockImplementation((userId, authField) => { @@ -78,9 +72,36 @@ describe('Tool Handlers', () => { }); afterAll(async () => { - await mockUser.findByIdAndDelete(fakeUser._id); + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + // Clear mocks but not the database since we need the user to persist + jest.clearAllMocks(); + + // Reset the mock implementations + const userAuthValues = {}; + mockPluginService.getUserPluginAuthValue.mockImplementation((userId, authField) => { + return userAuthValues[`${userId}-${authField}`]; + }); + mockPluginService.updateUserPluginAuth.mockImplementation( + (userId, authField, _pluginKey, credential) => { + const fields = authField.split('||'); + fields.forEach((field) => { + userAuthValues[`${userId}-${field}`] = credential; + }); + }, + ); + + // Re-add the auth configs for the user for (const authConfig of authConfigs) { - await PluginService.deleteUserPluginAuth(fakeUser._id, authConfig.authField); + await PluginService.updateUserPluginAuth( + fakeUser._id, + authConfig.authField, + pluginKey, + mockCredential, + ); } }); @@ -218,7 +239,6 @@ describe('Tool Handlers', () => { try { await loadTool2(); } catch (error) { - // eslint-disable-next-line jest/no-conditional-expect expect(error).toBeDefined(); } }); diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js index cdbff85c5..17b23f1c1 100644 --- a/api/cache/banViolation.js +++ b/api/cache/banViolation.js @@ -1,8 +1,8 @@ +const { logger } = require('@librechat/data-schemas'); const { ViolationTypes } = require('librechat-data-provider'); const { isEnabled, math, removePorts } = require('~/server/utils'); const { deleteAllUserSessions } = require('~/models'); const getLogStores = require('./getLogStores'); -const { logger } = require('~/config'); const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {}; const interval = math(BAN_INTERVAL, 20); @@ -32,7 +32,6 @@ const banViolation = async (req, res, errorMessage) => { if (!isEnabled(BAN_VIOLATIONS)) { return; } - if (!errorMessage) { return; } @@ -51,7 +50,6 @@ const banViolation = async (req, res, errorMessage) => { const banLogs = getLogStores(ViolationTypes.BAN); const duration = errorMessage.duration || banLogs.opts.ttl; - if (duration <= 0) { return; } diff --git a/api/cache/banViolation.spec.js b/api/cache/banViolation.spec.js index 8fef16920..df9875349 100644 --- a/api/cache/banViolation.spec.js +++ b/api/cache/banViolation.spec.js @@ -1,48 +1,28 @@ +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); const banViolation = require('./banViolation'); -jest.mock('keyv'); -jest.mock('../models/Session'); -// Mocking the getLogStores function -jest.mock('./getLogStores', () => { - return jest.fn().mockImplementation(() => { - const EventEmitter = require('events'); - const { CacheKeys } = require('librechat-data-provider'); - const math = require('../server/utils/math'); - const mockGet = jest.fn(); - const mockSet = jest.fn(); - class KeyvMongo extends EventEmitter { - constructor(url = 'mongodb://127.0.0.1:27017', options) { - super(); - this.ttlSupport = false; - url = url ?? {}; - if (typeof url === 'string') { - url = { url }; - } - if (url.uri) { - url = { url: url.uri, ...url }; - } - this.opts = { - url, - collection: 'keyv', - ...url, - ...options, - }; - } - - get = mockGet; - set = mockSet; - } - - return new KeyvMongo('', { - namespace: CacheKeys.BANS, - ttl: math(process.env.BAN_DURATION, 7200000), - }); - }); -}); +// Mock deleteAllUserSessions since we're testing ban logic, not session deletion +jest.mock('~/models', () => ({ + ...jest.requireActual('~/models'), + deleteAllUserSessions: jest.fn().mockResolvedValue(true), +})); describe('banViolation', () => { + let mongoServer; let req, res, errorMessage; + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + beforeEach(() => { req = { ip: '127.0.0.1', @@ -55,7 +35,7 @@ describe('banViolation', () => { }; errorMessage = { type: 'someViolation', - user_id: '12345', + user_id: new mongoose.Types.ObjectId().toString(), // Use valid ObjectId prev_count: 0, violation_count: 0, }; diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index b0a6a822a..2478bf40d 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -1,7 +1,7 @@ const { Keyv } = require('keyv'); const { CacheKeys, ViolationTypes, Time } = require('librechat-data-provider'); const { logFile, violationFile } = require('./keyvFiles'); -const { math, isEnabled } = require('~/server/utils'); +const { isEnabled, math } = require('~/server/utils'); const keyvRedis = require('./keyvRedis'); const keyvMongo = require('./keyvMongo'); diff --git a/api/lib/db/connectDb.js b/api/db/connect.js similarity index 96% rename from api/lib/db/connectDb.js rename to api/db/connect.js index b8cbeb2ad..e88ffa51e 100644 --- a/api/lib/db/connectDb.js +++ b/api/db/connect.js @@ -39,7 +39,10 @@ async function connectDb() { }); } cached.conn = await cached.promise; + return cached.conn; } -module.exports = connectDb; +module.exports = { + connectDb, +}; diff --git a/api/db/index.js b/api/db/index.js new file mode 100644 index 000000000..5c29902f6 --- /dev/null +++ b/api/db/index.js @@ -0,0 +1,8 @@ +const mongoose = require('mongoose'); +const { createModels } = require('@librechat/data-schemas'); +const { connectDb } = require('./connect'); +const indexSync = require('./indexSync'); + +createModels(mongoose); + +module.exports = { connectDb, indexSync }; diff --git a/api/lib/db/indexSync.js b/api/db/indexSync.js similarity index 93% rename from api/lib/db/indexSync.js rename to api/db/indexSync.js index 75acd9d23..e8bcd55e3 100644 --- a/api/lib/db/indexSync.js +++ b/api/db/indexSync.js @@ -1,8 +1,11 @@ +const mongoose = require('mongoose'); const { MeiliSearch } = require('meilisearch'); -const { Conversation } = require('~/models/Conversation'); -const { Message } = require('~/models/Message'); +const { logger } = require('@librechat/data-schemas'); + const { isEnabled } = require('~/server/utils'); -const { logger } = require('~/config'); + +const Conversation = mongoose.models.Conversation; +const Message = mongoose.models.Message; const searchEnabled = isEnabled(process.env.SEARCH); const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC); @@ -29,7 +32,6 @@ async function indexSync() { if (!searchEnabled) { return; } - try { const client = MeiliSearchClient.getInstance(); diff --git a/api/db/models.js b/api/db/models.js new file mode 100644 index 000000000..fca132744 --- /dev/null +++ b/api/db/models.js @@ -0,0 +1,5 @@ +const mongoose = require('mongoose'); +const { createModels } = require('@librechat/data-schemas'); +const models = createModels(mongoose); + +module.exports = { ...models }; diff --git a/api/lib/db/index.js b/api/lib/db/index.js deleted file mode 100644 index fa7a460d0..000000000 --- a/api/lib/db/index.js +++ /dev/null @@ -1,4 +0,0 @@ -const connectDb = require('./connectDb'); -const indexSync = require('./indexSync'); - -module.exports = { connectDb, indexSync }; diff --git a/api/models/Action.js b/api/models/Action.js index 677b4d78d..20aa20a7e 100644 --- a/api/models/Action.js +++ b/api/models/Action.js @@ -1,7 +1,4 @@ -const mongoose = require('mongoose'); -const { actionSchema } = require('@librechat/data-schemas'); - -const Action = mongoose.model('action', actionSchema); +const { Action } = require('~/db/models'); /** * Update an action with new data without overwriting existing properties, diff --git a/api/models/Agent.js b/api/models/Agent.js index a2b325b5b..808dbf09e 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -1,6 +1,6 @@ const mongoose = require('mongoose'); const crypto = require('node:crypto'); -const { agentSchema } = require('@librechat/data-schemas'); +const { logger } = require('@librechat/data-schemas'); const { SystemRoles, Tools, actionDelimiter } = require('librechat-data-provider'); const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } = require('librechat-data-provider').Constants; @@ -13,9 +13,7 @@ const { } = require('./Project'); const getLogStores = require('~/cache/getLogStores'); const { getActions } = require('./Action'); -const { logger } = require('~/config'); - -const Agent = mongoose.model('agent', agentSchema); +const { Agent } = require('~/db/models'); /** * Create an agent with the provided data. @@ -481,7 +479,6 @@ const getListAgents = async (searchParameter) => { delete globalQuery.author; query = { $or: [globalQuery, query] }; } - const agents = ( await Agent.find(query, { id: 1, @@ -662,7 +659,6 @@ const generateActionMetadataHash = async (actionIds, actions) => { */ module.exports = { - Agent, getAgent, loadAgent, createAgent, diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js index 41b32ffa9..bfa5a1825 100644 --- a/api/models/Agent.spec.js +++ b/api/models/Agent.spec.js @@ -8,18 +8,22 @@ process.env.CREDS_IV = '0123456789abcdef'; const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); +const { agentSchema } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { - Agent, - addAgentResourceFile, - removeAgentResourceFiles, + getAgent, createAgent, updateAgent, - getAgent, deleteAgent, getListAgents, updateAgentProjects, + addAgentResourceFile, + removeAgentResourceFiles, } = require('./Agent'); +/** + * @type {import('mongoose').Model} + */ +let Agent; describe('Agent Resource File Operations', () => { let mongoServer; @@ -27,6 +31,7 @@ describe('Agent Resource File Operations', () => { beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); + Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); await mongoose.connect(mongoUri); }); diff --git a/api/models/Assistant.js b/api/models/Assistant.js index a8a5b9815..be94d35d7 100644 --- a/api/models/Assistant.js +++ b/api/models/Assistant.js @@ -1,7 +1,4 @@ -const mongoose = require('mongoose'); -const { assistantSchema } = require('@librechat/data-schemas'); - -const Assistant = mongoose.model('assistant', assistantSchema); +const { Assistant } = require('~/db/models'); /** * Update an assistant with new data without overwriting existing properties, diff --git a/api/models/Balance.js b/api/models/Balance.js deleted file mode 100644 index 226f6ef50..000000000 --- a/api/models/Balance.js +++ /dev/null @@ -1,4 +0,0 @@ -const mongoose = require('mongoose'); -const { balanceSchema } = require('@librechat/data-schemas'); - -module.exports = mongoose.model('Balance', balanceSchema); diff --git a/api/models/Banner.js b/api/models/Banner.js index 399a8e72e..42ad1599e 100644 --- a/api/models/Banner.js +++ b/api/models/Banner.js @@ -1,8 +1,5 @@ -const mongoose = require('mongoose'); -const logger = require('~/config/winston'); -const { bannerSchema } = require('@librechat/data-schemas'); - -const Banner = mongoose.model('Banner', bannerSchema); +const { logger } = require('@librechat/data-schemas'); +const { Banner } = require('~/db/models'); /** * Retrieves the current active banner. @@ -28,4 +25,4 @@ const getBanner = async (user) => { } }; -module.exports = { Banner, getBanner }; +module.exports = { getBanner }; diff --git a/api/models/Config.js b/api/models/Config.js deleted file mode 100644 index fefb84b8f..000000000 --- a/api/models/Config.js +++ /dev/null @@ -1,86 +0,0 @@ -const mongoose = require('mongoose'); -const { logger } = require('~/config'); - -const major = [0, 0]; -const minor = [0, 0]; -const patch = [0, 5]; - -const configSchema = mongoose.Schema( - { - tag: { - type: String, - required: true, - validate: { - validator: function (tag) { - const [part1, part2, part3] = tag.replace('v', '').split('.').map(Number); - - // Check if all parts are numbers - if (isNaN(part1) || isNaN(part2) || isNaN(part3)) { - return false; - } - - // Check if all parts are within their respective ranges - if (part1 < major[0] || part1 > major[1]) { - return false; - } - if (part2 < minor[0] || part2 > minor[1]) { - return false; - } - if (part3 < patch[0] || part3 > patch[1]) { - return false; - } - return true; - }, - message: 'Invalid tag value', - }, - }, - searchEnabled: { - type: Boolean, - default: false, - }, - usersEnabled: { - type: Boolean, - default: false, - }, - startupCounts: { - type: Number, - default: 0, - }, - }, - { timestamps: true }, -); - -// Instance method -configSchema.methods.incrementCount = function () { - this.startupCounts += 1; -}; - -// Static methods -configSchema.statics.findByTag = async function (tag) { - return await this.findOne({ tag }).lean(); -}; - -configSchema.statics.updateByTag = async function (tag, update) { - return await this.findOneAndUpdate({ tag }, update, { new: true }); -}; - -const Config = mongoose.models.Config || mongoose.model('Config', configSchema); - -module.exports = { - getConfigs: async (filter) => { - try { - return await Config.find(filter).lean(); - } catch (error) { - logger.error('Error getting configs', error); - return { config: 'Error getting configs' }; - } - }, - deleteConfigs: async (filter) => { - try { - return await Config.deleteMany(filter); - } catch (error) { - logger.error('Error deleting configs', error); - return { config: 'Error deleting configs' }; - } - }, -}; diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 51081a649..38e2cbb44 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -1,6 +1,6 @@ -const Conversation = require('./schema/convoSchema'); +const { logger } = require('@librechat/data-schemas'); const { getMessages, deleteMessages } = require('./Message'); -const logger = require('~/config/winston'); +const { Conversation } = require('~/db/models'); /** * Searches for a conversation by conversationId and returns a lean document with only conversationId and user. @@ -75,7 +75,6 @@ const getConvoFiles = async (conversationId) => { }; module.exports = { - Conversation, getConvoFiles, searchConversation, deleteNullOrEmptyConversations, @@ -155,7 +154,6 @@ module.exports = { { cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {}, ) => { const filters = [{ user }]; - if (isArchived) { filters.push({ isArchived: true }); } else { @@ -288,7 +286,6 @@ module.exports = { deleteConvos: async (user, filter) => { try { const userFilter = { ...filter, user }; - const conversations = await Conversation.find(userFilter).select('conversationId'); const conversationIds = conversations.map((c) => c.conversationId); diff --git a/api/models/ConversationTag.js b/api/models/ConversationTag.js index f0cac8620..e6dc96be6 100644 --- a/api/models/ConversationTag.js +++ b/api/models/ConversationTag.js @@ -1,10 +1,5 @@ -const mongoose = require('mongoose'); -const Conversation = require('./schema/convoSchema'); -const logger = require('~/config/winston'); - -const { conversationTagSchema } = require('@librechat/data-schemas'); - -const ConversationTag = mongoose.model('ConversationTag', conversationTagSchema); +const { logger } = require('@librechat/data-schemas'); +const { ConversationTag, Conversation } = require('~/db/models'); /** * Retrieves all conversation tags for a user. @@ -140,13 +135,13 @@ const adjustPositions = async (user, oldPosition, newPosition) => { const position = oldPosition < newPosition ? { - $gt: Math.min(oldPosition, newPosition), - $lte: Math.max(oldPosition, newPosition), - } + $gt: Math.min(oldPosition, newPosition), + $lte: Math.max(oldPosition, newPosition), + } : { - $gte: Math.min(oldPosition, newPosition), - $lt: Math.max(oldPosition, newPosition), - }; + $gte: Math.min(oldPosition, newPosition), + $lt: Math.max(oldPosition, newPosition), + }; await ConversationTag.updateMany( { diff --git a/api/models/File.js b/api/models/File.js index 4d9499447..ff509539e 100644 --- a/api/models/File.js +++ b/api/models/File.js @@ -1,9 +1,6 @@ -const mongoose = require('mongoose'); +const { logger } = require('@librechat/data-schemas'); const { EToolResources } = require('librechat-data-provider'); -const { fileSchema } = require('@librechat/data-schemas'); -const { logger } = require('~/config'); - -const File = mongoose.model('File', fileSchema); +const { File } = require('~/db/models'); /** * Finds a file by its file_id with additional query options. @@ -169,7 +166,6 @@ async function batchUpdateFiles(updates) { } module.exports = { - File, findFileById, getFiles, getToolFilesByIds, diff --git a/api/models/Key.js b/api/models/Key.js deleted file mode 100644 index c69c350a4..000000000 --- a/api/models/Key.js +++ /dev/null @@ -1,4 +0,0 @@ -const mongoose = require('mongoose'); -const { keySchema } = require('@librechat/data-schemas'); - -module.exports = mongoose.model('Key', keySchema); diff --git a/api/models/Message.js b/api/models/Message.js index 9384c35f7..abd538084 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -1,6 +1,6 @@ const { z } = require('zod'); -const Message = require('./schema/messageSchema'); -const { logger } = require('~/config'); +const { logger } = require('@librechat/data-schemas'); +const { Message } = require('~/db/models'); const idSchema = z.string().uuid(); @@ -68,7 +68,6 @@ async function saveMessage(req, params, metadata) { logger.info(`---\`saveMessage\` context: ${metadata?.context}`); update.tokenCount = 0; } - const message = await Message.findOneAndUpdate( { messageId: params.messageId, user: req.user.id }, update, @@ -140,7 +139,6 @@ async function bulkSaveMessages(messages, overrideTimestamp = false) { upsert: true, }, })); - const result = await Message.bulkWrite(bulkOps); return result; } catch (err) { @@ -356,7 +354,6 @@ async function deleteMessages(filter) { } module.exports = { - Message, saveMessage, bulkSaveMessages, recordMessage, diff --git a/api/models/Message.spec.js b/api/models/Message.spec.js index 7aef84367..aebaebb44 100644 --- a/api/models/Message.spec.js +++ b/api/models/Message.spec.js @@ -1,32 +1,7 @@ const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); const { v4: uuidv4 } = require('uuid'); - -jest.mock('mongoose'); - -const mockFindQuery = { - select: jest.fn().mockReturnThis(), - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - deleteMany: jest.fn().mockResolvedValue({ deletedCount: 1 }), -}; - -const mockSchema = { - findOneAndUpdate: jest.fn(), - updateOne: jest.fn(), - findOne: jest.fn(() => ({ - lean: jest.fn(), - })), - find: jest.fn(() => mockFindQuery), - deleteMany: jest.fn(), -}; - -mongoose.model.mockReturnValue(mockSchema); - -jest.mock('~/models/schema/messageSchema', () => mockSchema); - -jest.mock('~/config/winston', () => ({ - error: jest.fn(), -})); +const { messageSchema } = require('@librechat/data-schemas'); const { saveMessage, @@ -35,77 +10,102 @@ const { deleteMessages, updateMessageText, deleteMessagesSince, -} = require('~/models/Message'); +} = require('./Message'); + +/** + * @type {import('mongoose').Model} + */ +let Message; describe('Message Operations', () => { + let mongoServer; let mockReq; - let mockMessage; + let mockMessageData; - beforeEach(() => { - jest.clearAllMocks(); + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + Message = mongoose.models.Message || mongoose.model('Message', messageSchema); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + // Clear database + await Message.deleteMany({}); mockReq = { user: { id: 'user123' }, }; - mockMessage = { + mockMessageData = { messageId: 'msg123', conversationId: uuidv4(), text: 'Hello, world!', user: 'user123', }; - - mockSchema.findOneAndUpdate.mockResolvedValue({ - toObject: () => mockMessage, - }); }); describe('saveMessage', () => { it('should save a message for an authenticated user', async () => { - const result = await saveMessage(mockReq, mockMessage); - expect(result).toEqual(mockMessage); - expect(mockSchema.findOneAndUpdate).toHaveBeenCalledWith( - { messageId: 'msg123', user: 'user123' }, - expect.objectContaining({ user: 'user123' }), - expect.any(Object), - ); + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.messageId).toBe('msg123'); + expect(result.user).toBe('user123'); + expect(result.text).toBe('Hello, world!'); + + // Verify the message was actually saved to the database + const savedMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); + expect(savedMessage).toBeTruthy(); + expect(savedMessage.text).toBe('Hello, world!'); }); it('should throw an error for unauthenticated user', async () => { mockReq.user = null; - await expect(saveMessage(mockReq, mockMessage)).rejects.toThrow('User not authenticated'); + await expect(saveMessage(mockReq, mockMessageData)).rejects.toThrow('User not authenticated'); }); - it('should throw an error for invalid conversation ID', async () => { - mockMessage.conversationId = 'invalid-id'; - await expect(saveMessage(mockReq, mockMessage)).resolves.toBeUndefined(); + it('should handle invalid conversation ID gracefully', async () => { + mockMessageData.conversationId = 'invalid-id'; + const result = await saveMessage(mockReq, mockMessageData); + expect(result).toBeUndefined(); }); }); describe('updateMessageText', () => { it('should update message text for the authenticated user', async () => { + // First save a message + await saveMessage(mockReq, mockMessageData); + + // Then update it await updateMessageText(mockReq, { messageId: 'msg123', text: 'Updated text' }); - expect(mockSchema.updateOne).toHaveBeenCalledWith( - { messageId: 'msg123', user: 'user123' }, - { text: 'Updated text' }, - ); + + // Verify the update + const updatedMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); + expect(updatedMessage.text).toBe('Updated text'); }); }); describe('updateMessage', () => { it('should update a message for the authenticated user', async () => { - mockSchema.findOneAndUpdate.mockResolvedValue(mockMessage); + // First save a message + await saveMessage(mockReq, mockMessageData); + const result = await updateMessage(mockReq, { messageId: 'msg123', text: 'Updated text' }); - expect(result).toEqual( - expect.objectContaining({ - messageId: 'msg123', - text: 'Hello, world!', - }), - ); + + expect(result.messageId).toBe('msg123'); + expect(result.text).toBe('Updated text'); + + // Verify in database + const updatedMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); + expect(updatedMessage.text).toBe('Updated text'); }); it('should throw an error if message is not found', async () => { - mockSchema.findOneAndUpdate.mockResolvedValue(null); await expect( updateMessage(mockReq, { messageId: 'nonexistent', text: 'Test' }), ).rejects.toThrow('Message not found or user not authorized.'); @@ -114,19 +114,45 @@ describe('Message Operations', () => { describe('deleteMessagesSince', () => { it('should delete messages only for the authenticated user', async () => { - mockSchema.findOne().lean.mockResolvedValueOnce({ createdAt: new Date() }); - mockFindQuery.deleteMany.mockResolvedValueOnce({ deletedCount: 1 }); - const result = await deleteMessagesSince(mockReq, { - messageId: 'msg123', - conversationId: 'convo123', + const conversationId = uuidv4(); + + // Create multiple messages in the same conversation + const message1 = await saveMessage(mockReq, { + messageId: 'msg1', + conversationId, + text: 'First message', + user: 'user123', }); - expect(mockSchema.findOne).toHaveBeenCalledWith({ messageId: 'msg123', user: 'user123' }); - expect(mockSchema.find).not.toHaveBeenCalled(); - expect(result).toBeUndefined(); + + const message2 = await saveMessage(mockReq, { + messageId: 'msg2', + conversationId, + text: 'Second message', + user: 'user123', + }); + + const message3 = await saveMessage(mockReq, { + messageId: 'msg3', + conversationId, + text: 'Third message', + user: 'user123', + }); + + // Delete messages since message2 (this should only delete messages created AFTER msg2) + await deleteMessagesSince(mockReq, { + messageId: 'msg2', + conversationId, + }); + + // Verify msg1 and msg2 remain, msg3 is deleted + const remainingMessages = await Message.find({ conversationId, user: 'user123' }); + expect(remainingMessages).toHaveLength(2); + expect(remainingMessages.map((m) => m.messageId)).toContain('msg1'); + expect(remainingMessages.map((m) => m.messageId)).toContain('msg2'); + expect(remainingMessages.map((m) => m.messageId)).not.toContain('msg3'); }); it('should return undefined if no message is found', async () => { - mockSchema.findOne().lean.mockResolvedValueOnce(null); const result = await deleteMessagesSince(mockReq, { messageId: 'nonexistent', conversationId: 'convo123', @@ -137,29 +163,71 @@ describe('Message Operations', () => { describe('getMessages', () => { it('should retrieve messages with the correct filter', async () => { - const filter = { conversationId: 'convo123' }; - await getMessages(filter); - expect(mockSchema.find).toHaveBeenCalledWith(filter); - expect(mockFindQuery.sort).toHaveBeenCalledWith({ createdAt: 1 }); - expect(mockFindQuery.lean).toHaveBeenCalled(); + const conversationId = uuidv4(); + + // Save some messages + await saveMessage(mockReq, { + messageId: 'msg1', + conversationId, + text: 'First message', + user: 'user123', + }); + + await saveMessage(mockReq, { + messageId: 'msg2', + conversationId, + text: 'Second message', + user: 'user123', + }); + + const messages = await getMessages({ conversationId }); + expect(messages).toHaveLength(2); + expect(messages[0].text).toBe('First message'); + expect(messages[1].text).toBe('Second message'); }); }); describe('deleteMessages', () => { it('should delete messages with the correct filter', async () => { + // Save some messages for different users + await saveMessage(mockReq, mockMessageData); + await saveMessage( + { user: { id: 'user456' } }, + { + messageId: 'msg456', + conversationId: uuidv4(), + text: 'Other user message', + user: 'user456', + }, + ); + await deleteMessages({ user: 'user123' }); - expect(mockSchema.deleteMany).toHaveBeenCalledWith({ user: 'user123' }); + + // Verify only user123's messages were deleted + const user123Messages = await Message.find({ user: 'user123' }); + const user456Messages = await Message.find({ user: 'user456' }); + + expect(user123Messages).toHaveLength(0); + expect(user456Messages).toHaveLength(1); }); }); describe('Conversation Hijacking Prevention', () => { it("should not allow editing a message in another user's conversation", async () => { const attackerReq = { user: { id: 'attacker123' } }; - const victimConversationId = 'victim-convo-123'; + const victimConversationId = uuidv4(); const victimMessageId = 'victim-msg-123'; - mockSchema.findOneAndUpdate.mockResolvedValue(null); + // First, save a message as the victim (but we'll try to edit as attacker) + const victimReq = { user: { id: 'victim123' } }; + await saveMessage(victimReq, { + messageId: victimMessageId, + conversationId: victimConversationId, + text: 'Victim message', + user: 'victim123', + }); + // Attacker tries to edit the victim's message await expect( updateMessage(attackerReq, { messageId: victimMessageId, @@ -168,71 +236,82 @@ describe('Message Operations', () => { }), ).rejects.toThrow('Message not found or user not authorized.'); - expect(mockSchema.findOneAndUpdate).toHaveBeenCalledWith( - { messageId: victimMessageId, user: 'attacker123' }, - expect.anything(), - expect.anything(), - ); + // Verify the original message is unchanged + const originalMessage = await Message.findOne({ + messageId: victimMessageId, + user: 'victim123', + }); + expect(originalMessage.text).toBe('Victim message'); }); it("should not allow deleting messages from another user's conversation", async () => { const attackerReq = { user: { id: 'attacker123' } }; - const victimConversationId = 'victim-convo-123'; + const victimConversationId = uuidv4(); const victimMessageId = 'victim-msg-123'; - mockSchema.findOne().lean.mockResolvedValueOnce(null); // Simulating message not found for this user + // Save a message as the victim + const victimReq = { user: { id: 'victim123' } }; + await saveMessage(victimReq, { + messageId: victimMessageId, + conversationId: victimConversationId, + text: 'Victim message', + user: 'victim123', + }); + + // Attacker tries to delete from victim's conversation const result = await deleteMessagesSince(attackerReq, { messageId: victimMessageId, conversationId: victimConversationId, }); expect(result).toBeUndefined(); - expect(mockSchema.findOne).toHaveBeenCalledWith({ + + // Verify the victim's message still exists + const victimMessage = await Message.findOne({ messageId: victimMessageId, - user: 'attacker123', + user: 'victim123', }); + expect(victimMessage).toBeTruthy(); + expect(victimMessage.text).toBe('Victim message'); }); it("should not allow inserting a new message into another user's conversation", async () => { const attackerReq = { user: { id: 'attacker123' } }; - const victimConversationId = uuidv4(); // Use a valid UUID + const victimConversationId = uuidv4(); - await expect( - saveMessage(attackerReq, { - conversationId: victimConversationId, - text: 'Inserted malicious message', - messageId: 'new-msg-123', - }), - ).resolves.not.toThrow(); // It should not throw an error + // Attacker tries to save a message - this should succeed but with attacker's user ID + const result = await saveMessage(attackerReq, { + conversationId: victimConversationId, + text: 'Inserted malicious message', + messageId: 'new-msg-123', + user: 'attacker123', + }); - // Check that the message was saved with the attacker's user ID - expect(mockSchema.findOneAndUpdate).toHaveBeenCalledWith( - { messageId: 'new-msg-123', user: 'attacker123' }, - expect.objectContaining({ - user: 'attacker123', - conversationId: victimConversationId, - }), - expect.anything(), - ); + expect(result).toBeTruthy(); + expect(result.user).toBe('attacker123'); + + // Verify the message was saved with the attacker's user ID, not as an anonymous message + const savedMessage = await Message.findOne({ messageId: 'new-msg-123' }); + expect(savedMessage.user).toBe('attacker123'); + expect(savedMessage.conversationId).toBe(victimConversationId); }); it('should allow retrieving messages from any conversation', async () => { - const victimConversationId = 'victim-convo-123'; + const victimConversationId = uuidv4(); - await getMessages({ conversationId: victimConversationId }); - - expect(mockSchema.find).toHaveBeenCalledWith({ + // Save a message in the victim's conversation + const victimReq = { user: { id: 'victim123' } }; + await saveMessage(victimReq, { + messageId: 'victim-msg', conversationId: victimConversationId, + text: 'Victim message', + user: 'victim123', }); - mockSchema.find.mockReturnValueOnce({ - select: jest.fn().mockReturnThis(), - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockResolvedValue([{ text: 'Test message' }]), - }); - - const result = await getMessages({ conversationId: victimConversationId }); - expect(result).toEqual([{ text: 'Test message' }]); + // Anyone should be able to retrieve messages by conversation ID + const messages = await getMessages({ conversationId: victimConversationId }); + expect(messages).toHaveLength(1); + expect(messages[0].text).toBe('Victim message'); }); }); }); diff --git a/api/models/Preset.js b/api/models/Preset.js index 970b2958f..4db3d5906 100644 --- a/api/models/Preset.js +++ b/api/models/Preset.js @@ -1,5 +1,5 @@ -const Preset = require('./schema/presetSchema'); -const { logger } = require('~/config'); +const { logger } = require('@librechat/data-schemas'); +const { Preset } = require('~/db/models'); const getPreset = async (user, presetId) => { try { @@ -11,7 +11,6 @@ const getPreset = async (user, presetId) => { }; module.exports = { - Preset, getPreset, getPresets: async (user, filter) => { try { diff --git a/api/models/Project.js b/api/models/Project.js index 43d726372..8fd1e556f 100644 --- a/api/models/Project.js +++ b/api/models/Project.js @@ -1,8 +1,5 @@ -const { model } = require('mongoose'); const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; -const { projectSchema } = require('@librechat/data-schemas'); - -const Project = model('Project', projectSchema); +const { Project } = require('~/db/models'); /** * Retrieve a project by ID and convert the found project document to a plain object. diff --git a/api/models/Prompt.js b/api/models/Prompt.js index 43dc3ec22..9499e19c8 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -1,5 +1,5 @@ -const mongoose = require('mongoose'); const { ObjectId } = require('mongodb'); +const { logger } = require('@librechat/data-schemas'); const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider'); const { getProjectByName, @@ -7,12 +7,8 @@ const { removeGroupIdsFromProject, removeGroupFromAllProjects, } = require('./Project'); -const { promptGroupSchema, promptSchema } = require('@librechat/data-schemas'); +const { PromptGroup, Prompt } = require('~/db/models'); const { escapeRegExp } = require('~/server/utils'); -const { logger } = require('~/config'); - -const PromptGroup = mongoose.model('PromptGroup', promptGroupSchema); -const Prompt = mongoose.model('Prompt', promptSchema); /** * Create a pipeline for the aggregation to get prompt groups diff --git a/api/models/Role.js b/api/models/Role.js index 07bf5a2cc..d7f1c0f9c 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -1,4 +1,3 @@ -const mongoose = require('mongoose'); const { CacheKeys, SystemRoles, @@ -7,11 +6,9 @@ const { permissionsSchema, removeNullishValues, } = require('librechat-data-provider'); +const { logger } = require('@librechat/data-schemas'); const getLogStores = require('~/cache/getLogStores'); -const { roleSchema } = require('@librechat/data-schemas'); -const { logger } = require('~/config'); - -const Role = mongoose.model('Role', roleSchema); +const { Role } = require('~/db/models'); /** * Retrieve a role by name and convert the found role document to a plain object. @@ -173,35 +170,6 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { } } -/** - * Initialize default roles in the system. - * Creates the default roles (ADMIN, USER) if they don't exist in the database. - * Updates existing roles with new permission types if they're missing. - * - * @returns {Promise} - */ -const initializeRoles = async function () { - for (const roleName of [SystemRoles.ADMIN, SystemRoles.USER]) { - let role = await Role.findOne({ name: roleName }); - const defaultPerms = roleDefaults[roleName].permissions; - - if (!role) { - // Create new role if it doesn't exist. - role = new Role(roleDefaults[roleName]); - } else { - // Ensure role.permissions is defined. - role.permissions = role.permissions || {}; - // For each permission type in defaults, add it if missing. - for (const permType of Object.keys(defaultPerms)) { - if (role.permissions[permType] == null) { - role.permissions[permType] = defaultPerms[permType]; - } - } - } - await role.save(); - } -}; - /** * Migrates roles from old schema to new schema structure. * This can be called directly to fix existing roles. @@ -282,10 +250,8 @@ const migrateRoleSchema = async function (roleName) { }; module.exports = { - Role, getRoleByName, - initializeRoles, updateRoleByName, - updateAccessPermissions, migrateRoleSchema, + updateAccessPermissions, }; diff --git a/api/models/Role.spec.js b/api/models/Role.spec.js index a8b60801c..c344f719d 100644 --- a/api/models/Role.spec.js +++ b/api/models/Role.spec.js @@ -6,8 +6,10 @@ const { roleDefaults, PermissionTypes, } = require('librechat-data-provider'); -const { Role, getRoleByName, updateAccessPermissions, initializeRoles } = require('~/models/Role'); +const { getRoleByName, updateAccessPermissions } = require('~/models/Role'); const getLogStores = require('~/cache/getLogStores'); +const { initializeRoles } = require('~/models'); +const { Role } = require('~/db/models'); // Mock the cache jest.mock('~/cache/getLogStores', () => diff --git a/api/models/Session.js b/api/models/Session.js deleted file mode 100644 index 38821b77d..000000000 --- a/api/models/Session.js +++ /dev/null @@ -1,275 +0,0 @@ -const mongoose = require('mongoose'); -const signPayload = require('~/server/services/signPayload'); -const { hashToken } = require('~/server/utils/crypto'); -const { sessionSchema } = require('@librechat/data-schemas'); -const { logger } = require('~/config'); - -const Session = mongoose.model('Session', sessionSchema); - -const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; -const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default - -/** - * Error class for Session-related errors - */ -class SessionError extends Error { - constructor(message, code = 'SESSION_ERROR') { - super(message); - this.name = 'SessionError'; - this.code = code; - } -} - -/** - * Creates a new session for a user - * @param {string} userId - The ID of the user - * @param {Object} options - Additional options for session creation - * @param {Date} options.expiration - Custom expiration date - * @returns {Promise<{session: Session, refreshToken: string}>} - * @throws {SessionError} - */ -const createSession = async (userId, options = {}) => { - if (!userId) { - throw new SessionError('User ID is required', 'INVALID_USER_ID'); - } - - try { - const session = new Session({ - user: userId, - expiration: options.expiration || new Date(Date.now() + expires), - }); - const refreshToken = await generateRefreshToken(session); - return { session, refreshToken }; - } catch (error) { - logger.error('[createSession] Error creating session:', error); - throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED'); - } -}; - -/** - * Finds a session by various parameters - * @param {Object} params - Search parameters - * @param {string} [params.refreshToken] - The refresh token to search by - * @param {string} [params.userId] - The user ID to search by - * @param {string} [params.sessionId] - The session ID to search by - * @param {Object} [options] - Additional options - * @param {boolean} [options.lean=true] - Whether to return plain objects instead of documents - * @returns {Promise} - * @throws {SessionError} - */ -const findSession = async (params, options = { lean: true }) => { - try { - const query = {}; - - if (!params.refreshToken && !params.userId && !params.sessionId) { - throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS'); - } - - if (params.refreshToken) { - const tokenHash = await hashToken(params.refreshToken); - query.refreshTokenHash = tokenHash; - } - - if (params.userId) { - query.user = params.userId; - } - - if (params.sessionId) { - const sessionId = params.sessionId.sessionId || params.sessionId; - if (!mongoose.Types.ObjectId.isValid(sessionId)) { - throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID'); - } - query._id = sessionId; - } - - // Add expiration check to only return valid sessions - query.expiration = { $gt: new Date() }; - - const sessionQuery = Session.findOne(query); - - if (options.lean) { - return await sessionQuery.lean(); - } - - return await sessionQuery.exec(); - } catch (error) { - logger.error('[findSession] Error finding session:', error); - throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED'); - } -}; - -/** - * Updates session expiration - * @param {Session|string} session - The session or session ID to update - * @param {Date} [newExpiration] - Optional new expiration date - * @returns {Promise} - * @throws {SessionError} - */ -const updateExpiration = async (session, newExpiration) => { - try { - const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session; - - if (!sessionDoc) { - throw new SessionError('Session not found', 'SESSION_NOT_FOUND'); - } - - sessionDoc.expiration = newExpiration || new Date(Date.now() + expires); - return await sessionDoc.save(); - } catch (error) { - logger.error('[updateExpiration] Error updating session:', error); - throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED'); - } -}; - -/** - * Deletes a session by refresh token or session ID - * @param {Object} params - Delete parameters - * @param {string} [params.refreshToken] - The refresh token of the session to delete - * @param {string} [params.sessionId] - The ID of the session to delete - * @returns {Promise} - * @throws {SessionError} - */ -const deleteSession = async (params) => { - try { - if (!params.refreshToken && !params.sessionId) { - throw new SessionError( - 'Either refreshToken or sessionId is required', - 'INVALID_DELETE_PARAMS', - ); - } - - const query = {}; - - if (params.refreshToken) { - query.refreshTokenHash = await hashToken(params.refreshToken); - } - - if (params.sessionId) { - query._id = params.sessionId; - } - - const result = await Session.deleteOne(query); - - if (result.deletedCount === 0) { - logger.warn('[deleteSession] No session found to delete'); - } - - return result; - } catch (error) { - logger.error('[deleteSession] Error deleting session:', error); - throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED'); - } -}; - -/** - * Deletes all sessions for a user - * @param {string} userId - The ID of the user - * @param {Object} [options] - Additional options - * @param {boolean} [options.excludeCurrentSession] - Whether to exclude the current session - * @param {string} [options.currentSessionId] - The ID of the current session to exclude - * @returns {Promise} - * @throws {SessionError} - */ -const deleteAllUserSessions = async (userId, options = {}) => { - try { - if (!userId) { - throw new SessionError('User ID is required', 'INVALID_USER_ID'); - } - - // Extract userId if it's passed as an object - const userIdString = userId.userId || userId; - - if (!mongoose.Types.ObjectId.isValid(userIdString)) { - throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT'); - } - - const query = { user: userIdString }; - - if (options.excludeCurrentSession && options.currentSessionId) { - query._id = { $ne: options.currentSessionId }; - } - - const result = await Session.deleteMany(query); - - if (result.deletedCount > 0) { - logger.debug( - `[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`, - ); - } - - return result; - } catch (error) { - logger.error('[deleteAllUserSessions] Error deleting user sessions:', error); - throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED'); - } -}; - -/** - * Generates a refresh token for a session - * @param {Session} session - The session to generate a token for - * @returns {Promise} - * @throws {SessionError} - */ -const generateRefreshToken = async (session) => { - if (!session || !session.user) { - throw new SessionError('Invalid session object', 'INVALID_SESSION'); - } - - try { - const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires; - - if (!session.expiration) { - session.expiration = new Date(expiresIn); - } - - const refreshToken = await signPayload({ - payload: { - id: session.user, - sessionId: session._id, - }, - secret: process.env.JWT_REFRESH_SECRET, - expirationTime: Math.floor((expiresIn - Date.now()) / 1000), - }); - - session.refreshTokenHash = await hashToken(refreshToken); - await session.save(); - - return refreshToken; - } catch (error) { - logger.error('[generateRefreshToken] Error generating refresh token:', error); - throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED'); - } -}; - -/** - * Counts active sessions for a user - * @param {string} userId - The ID of the user - * @returns {Promise} - * @throws {SessionError} - */ -const countActiveSessions = async (userId) => { - try { - if (!userId) { - throw new SessionError('User ID is required', 'INVALID_USER_ID'); - } - - return await Session.countDocuments({ - user: userId, - expiration: { $gt: new Date() }, - }); - } catch (error) { - logger.error('[countActiveSessions] Error counting active sessions:', error); - throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED'); - } -}; - -module.exports = { - createSession, - findSession, - updateExpiration, - deleteSession, - deleteAllUserSessions, - generateRefreshToken, - countActiveSessions, - SessionError, -}; diff --git a/api/models/Share.js b/api/models/Share.js index 8611d01bc..f8712c36a 100644 --- a/api/models/Share.js +++ b/api/models/Share.js @@ -1,11 +1,8 @@ -const mongoose = require('mongoose'); const { nanoid } = require('nanoid'); +const { logger } = require('@librechat/data-schemas'); const { Constants } = require('librechat-data-provider'); -const { Conversation } = require('~/models/Conversation'); -const { shareSchema } = require('@librechat/data-schemas'); -const SharedLink = mongoose.model('SharedLink', shareSchema); +const { Conversation, SharedLink } = require('~/db/models'); const { getMessages } = require('./Message'); -const logger = require('~/config/winston'); class ShareServiceError extends Error { constructor(message, code) { @@ -202,7 +199,6 @@ async function createSharedLink(user, conversationId) { if (!user || !conversationId) { throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS'); } - try { const [existingShare, conversationMessages] = await Promise.all([ SharedLink.findOne({ conversationId, isPublic: true }).select('-_id -__v -user').lean(), @@ -340,7 +336,6 @@ async function deleteSharedLink(user, shareId) { } module.exports = { - SharedLink, getSharedLink, getSharedLinks, createSharedLink, diff --git a/api/models/Token.js b/api/models/Token.js index c89abb8c8..6f130eb2c 100644 --- a/api/models/Token.js +++ b/api/models/Token.js @@ -1,158 +1,5 @@ -const mongoose = require('mongoose'); +const { findToken, updateToken, createToken } = require('~/models'); const { encryptV2 } = require('~/server/utils/crypto'); -const { tokenSchema } = require('@librechat/data-schemas'); -const { logger } = require('~/config'); - -/** - * Token model. - * @type {mongoose.Model} - */ -const Token = mongoose.model('Token', tokenSchema); -/** - * Fixes the indexes for the Token collection from legacy TTL indexes to the new expiresAt index. - */ -async function fixIndexes() { - try { - if ( - process.env.NODE_ENV === 'CI' || - process.env.NODE_ENV === 'development' || - process.env.NODE_ENV === 'test' - ) { - return; - } - const indexes = await Token.collection.indexes(); - logger.debug('Existing Token Indexes:', JSON.stringify(indexes, null, 2)); - const unwantedTTLIndexes = indexes.filter( - (index) => index.key.createdAt === 1 && index.expireAfterSeconds !== undefined, - ); - if (unwantedTTLIndexes.length === 0) { - logger.debug('No unwanted Token indexes found.'); - return; - } - for (const index of unwantedTTLIndexes) { - logger.debug(`Dropping unwanted Token index: ${index.name}`); - await Token.collection.dropIndex(index.name); - logger.debug(`Dropped Token index: ${index.name}`); - } - logger.debug('Token index cleanup completed successfully.'); - } catch (error) { - logger.error('An error occurred while fixing Token indexes:', error); - } -} - -fixIndexes(); - -/** - * Creates a new Token instance. - * @param {Object} tokenData - The data for the new Token. - * @param {mongoose.Types.ObjectId} tokenData.userId - The user's ID. It is required. - * @param {String} tokenData.email - The user's email. - * @param {String} tokenData.token - The token. It is required. - * @param {Number} tokenData.expiresIn - The number of seconds until the token expires. - * @returns {Promise} The new Token instance. - * @throws Will throw an error if token creation fails. - */ -async function createToken(tokenData) { - try { - const currentTime = new Date(); - const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000); - - const newTokenData = { - ...tokenData, - createdAt: currentTime, - expiresAt, - }; - - return await Token.create(newTokenData); - } catch (error) { - logger.debug('An error occurred while creating token:', error); - throw error; - } -} - -/** - * Finds a Token document that matches the provided query. - * @param {Object} query - The query to match against. - * @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user. - * @param {String} query.token - The token value. - * @param {String} [query.email] - The email of the user. - * @param {String} [query.identifier] - Unique, alternative identifier for the token. - * @returns {Promise} The matched Token document, or null if not found. - * @throws Will throw an error if the find operation fails. - */ -async function findToken(query) { - try { - const conditions = []; - - if (query.userId) { - conditions.push({ userId: query.userId }); - } - if (query.token) { - conditions.push({ token: query.token }); - } - if (query.email) { - conditions.push({ email: query.email }); - } - if (query.identifier) { - conditions.push({ identifier: query.identifier }); - } - - const token = await Token.findOne({ - $and: conditions, - }).lean(); - - return token; - } catch (error) { - logger.debug('An error occurred while finding token:', error); - throw error; - } -} - -/** - * Updates a Token document that matches the provided query. - * @param {Object} query - The query to match against. - * @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user. - * @param {String} query.token - The token value. - * @param {String} [query.email] - The email of the user. - * @param {String} [query.identifier] - Unique, alternative identifier for the token. - * @param {Object} updateData - The data to update the Token with. - * @returns {Promise} The updated Token document, or null if not found. - * @throws Will throw an error if the update operation fails. - */ -async function updateToken(query, updateData) { - try { - return await Token.findOneAndUpdate(query, updateData, { new: true }); - } catch (error) { - logger.debug('An error occurred while updating token:', error); - throw error; - } -} - -/** - * Deletes all Token documents that match the provided token, user ID, or email. - * @param {Object} query - The query to match against. - * @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user. - * @param {String} query.token - The token value. - * @param {String} [query.email] - The email of the user. - * @param {String} [query.identifier] - Unique, alternative identifier for the token. - * @returns {Promise} The result of the delete operation. - * @throws Will throw an error if the delete operation fails. - */ -async function deleteTokens(query) { - try { - return await Token.deleteMany({ - $or: [ - { userId: query.userId }, - { token: query.token }, - { email: query.email }, - { identifier: query.identifier }, - ], - }); - } catch (error) { - logger.debug('An error occurred while deleting tokens:', error); - throw error; - } -} /** * Handles the OAuth token by creating or updating the token. @@ -191,9 +38,5 @@ async function handleOAuthToken({ } module.exports = { - findToken, - createToken, - updateToken, - deleteTokens, handleOAuthToken, }; diff --git a/api/models/ToolCall.js b/api/models/ToolCall.js index 7bc0f157d..689386114 100644 --- a/api/models/ToolCall.js +++ b/api/models/ToolCall.js @@ -1,6 +1,4 @@ -const mongoose = require('mongoose'); -const { toolCallSchema } = require('@librechat/data-schemas'); -const ToolCall = mongoose.model('ToolCall', toolCallSchema); +const { ToolCall } = require('~/db/models'); /** * Create a new tool call diff --git a/api/models/Transaction.js b/api/models/Transaction.js index e171241b6..0e0e32785 100644 --- a/api/models/Transaction.js +++ b/api/models/Transaction.js @@ -1,9 +1,7 @@ -const mongoose = require('mongoose'); -const { transactionSchema } = require('@librechat/data-schemas'); +const { logger } = require('@librechat/data-schemas'); const { getBalanceConfig } = require('~/server/services/Config'); const { getMultiplier, getCacheMultiplier } = require('./tx'); -const { logger } = require('~/config'); -const Balance = require('./Balance'); +const { Transaction, Balance } = require('~/db/models'); const cancelRate = 1.15; @@ -140,19 +138,19 @@ const updateBalance = async ({ user, incrementValue, setValues }) => { }; /** Method to calculate and set the tokenValue for a transaction */ -transactionSchema.methods.calculateTokenValue = function () { - if (!this.valueKey || !this.tokenType) { - this.tokenValue = this.rawAmount; +function calculateTokenValue(txn) { + if (!txn.valueKey || !txn.tokenType) { + txn.tokenValue = txn.rawAmount; } - const { valueKey, tokenType, model, endpointTokenConfig } = this; + const { valueKey, tokenType, model, endpointTokenConfig } = txn; const multiplier = Math.abs(getMultiplier({ valueKey, tokenType, model, endpointTokenConfig })); - this.rate = multiplier; - this.tokenValue = this.rawAmount * multiplier; - if (this.context && this.tokenType === 'completion' && this.context === 'incomplete') { - this.tokenValue = Math.ceil(this.tokenValue * cancelRate); - this.rate *= cancelRate; + txn.rate = multiplier; + txn.tokenValue = txn.rawAmount * multiplier; + if (txn.context && txn.tokenType === 'completion' && txn.context === 'incomplete') { + txn.tokenValue = Math.ceil(txn.tokenValue * cancelRate); + txn.rate *= cancelRate; } -}; +} /** * New static method to create an auto-refill transaction that does NOT trigger a balance update. @@ -163,13 +161,13 @@ transactionSchema.methods.calculateTokenValue = function () { * @param {number} txData.rawAmount - The raw amount of tokens. * @returns {Promise} - The created transaction. */ -transactionSchema.statics.createAutoRefillTransaction = async function (txData) { +async function createAutoRefillTransaction(txData) { if (txData.rawAmount != null && isNaN(txData.rawAmount)) { return; } - const transaction = new this(txData); + const transaction = new Transaction(txData); transaction.endpointTokenConfig = txData.endpointTokenConfig; - transaction.calculateTokenValue(); + calculateTokenValue(transaction); await transaction.save(); const balanceResponse = await updateBalance({ @@ -185,21 +183,20 @@ transactionSchema.statics.createAutoRefillTransaction = async function (txData) logger.debug('[Balance.check] Auto-refill performed', result); result.transaction = transaction; return result; -}; +} /** * Static method to create a transaction and update the balance * @param {txData} txData - Transaction data. */ -transactionSchema.statics.create = async function (txData) { - const Transaction = this; +async function createTransaction(txData) { if (txData.rawAmount != null && isNaN(txData.rawAmount)) { return; } const transaction = new Transaction(txData); transaction.endpointTokenConfig = txData.endpointTokenConfig; - transaction.calculateTokenValue(); + calculateTokenValue(transaction); await transaction.save(); @@ -209,7 +206,6 @@ transactionSchema.statics.create = async function (txData) { } let incrementValue = transaction.tokenValue; - const balanceResponse = await updateBalance({ user: transaction.user, incrementValue, @@ -221,21 +217,19 @@ transactionSchema.statics.create = async function (txData) { balance: balanceResponse.tokenCredits, [transaction.tokenType]: incrementValue, }; -}; +} /** * Static method to create a structured transaction and update the balance * @param {txData} txData - Transaction data. */ -transactionSchema.statics.createStructured = async function (txData) { - const Transaction = this; - +async function createStructuredTransaction(txData) { const transaction = new Transaction({ ...txData, endpointTokenConfig: txData.endpointTokenConfig, }); - transaction.calculateStructuredTokenValue(); + calculateStructuredTokenValue(transaction); await transaction.save(); @@ -257,71 +251,69 @@ transactionSchema.statics.createStructured = async function (txData) { balance: balanceResponse.tokenCredits, [transaction.tokenType]: incrementValue, }; -}; +} /** Method to calculate token value for structured tokens */ -transactionSchema.methods.calculateStructuredTokenValue = function () { - if (!this.tokenType) { - this.tokenValue = this.rawAmount; +function calculateStructuredTokenValue(txn) { + if (!txn.tokenType) { + txn.tokenValue = txn.rawAmount; return; } - const { model, endpointTokenConfig } = this; + const { model, endpointTokenConfig } = txn; - if (this.tokenType === 'prompt') { + if (txn.tokenType === 'prompt') { const inputMultiplier = getMultiplier({ tokenType: 'prompt', model, endpointTokenConfig }); const writeMultiplier = getCacheMultiplier({ cacheType: 'write', model, endpointTokenConfig }) ?? inputMultiplier; const readMultiplier = getCacheMultiplier({ cacheType: 'read', model, endpointTokenConfig }) ?? inputMultiplier; - this.rateDetail = { + txn.rateDetail = { input: inputMultiplier, write: writeMultiplier, read: readMultiplier, }; const totalPromptTokens = - Math.abs(this.inputTokens || 0) + - Math.abs(this.writeTokens || 0) + - Math.abs(this.readTokens || 0); + Math.abs(txn.inputTokens || 0) + + Math.abs(txn.writeTokens || 0) + + Math.abs(txn.readTokens || 0); if (totalPromptTokens > 0) { - this.rate = - (Math.abs(inputMultiplier * (this.inputTokens || 0)) + - Math.abs(writeMultiplier * (this.writeTokens || 0)) + - Math.abs(readMultiplier * (this.readTokens || 0))) / + txn.rate = + (Math.abs(inputMultiplier * (txn.inputTokens || 0)) + + Math.abs(writeMultiplier * (txn.writeTokens || 0)) + + Math.abs(readMultiplier * (txn.readTokens || 0))) / totalPromptTokens; } else { - this.rate = Math.abs(inputMultiplier); // Default to input rate if no tokens + txn.rate = Math.abs(inputMultiplier); // Default to input rate if no tokens } - this.tokenValue = -( - Math.abs(this.inputTokens || 0) * inputMultiplier + - Math.abs(this.writeTokens || 0) * writeMultiplier + - Math.abs(this.readTokens || 0) * readMultiplier + txn.tokenValue = -( + Math.abs(txn.inputTokens || 0) * inputMultiplier + + Math.abs(txn.writeTokens || 0) * writeMultiplier + + Math.abs(txn.readTokens || 0) * readMultiplier ); - this.rawAmount = -totalPromptTokens; - } else if (this.tokenType === 'completion') { - const multiplier = getMultiplier({ tokenType: this.tokenType, model, endpointTokenConfig }); - this.rate = Math.abs(multiplier); - this.tokenValue = -Math.abs(this.rawAmount) * multiplier; - this.rawAmount = -Math.abs(this.rawAmount); + txn.rawAmount = -totalPromptTokens; + } else if (txn.tokenType === 'completion') { + const multiplier = getMultiplier({ tokenType: txn.tokenType, model, endpointTokenConfig }); + txn.rate = Math.abs(multiplier); + txn.tokenValue = -Math.abs(txn.rawAmount) * multiplier; + txn.rawAmount = -Math.abs(txn.rawAmount); } - if (this.context && this.tokenType === 'completion' && this.context === 'incomplete') { - this.tokenValue = Math.ceil(this.tokenValue * cancelRate); - this.rate *= cancelRate; - if (this.rateDetail) { - this.rateDetail = Object.fromEntries( - Object.entries(this.rateDetail).map(([k, v]) => [k, v * cancelRate]), + if (txn.context && txn.tokenType === 'completion' && txn.context === 'incomplete') { + txn.tokenValue = Math.ceil(txn.tokenValue * cancelRate); + txn.rate *= cancelRate; + if (txn.rateDetail) { + txn.rateDetail = Object.fromEntries( + Object.entries(txn.rateDetail).map(([k, v]) => [k, v * cancelRate]), ); } } -}; - -const Transaction = mongoose.model('Transaction', transactionSchema); +} /** * Queries and retrieves transactions based on a given filter. @@ -340,4 +332,9 @@ async function getTransactions(filter) { } } -module.exports = { Transaction, getTransactions }; +module.exports = { + getTransactions, + createTransaction, + createAutoRefillTransaction, + createStructuredTransaction, +}; diff --git a/api/models/Transaction.spec.js b/api/models/Transaction.spec.js index 43f3c004b..3a1303ede 100644 --- a/api/models/Transaction.spec.js +++ b/api/models/Transaction.spec.js @@ -3,14 +3,13 @@ const { MongoMemoryServer } = require('mongodb-memory-server'); const { spendTokens, spendStructuredTokens } = require('./spendTokens'); const { getBalanceConfig } = require('~/server/services/Config'); const { getMultiplier, getCacheMultiplier } = require('./tx'); -const { Transaction } = require('./Transaction'); -const Balance = require('./Balance'); +const { createTransaction } = require('./Transaction'); +const { Balance } = require('~/db/models'); // Mock the custom config module so we can control the balance flag. jest.mock('~/server/services/Config'); let mongoServer; - beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); @@ -368,7 +367,7 @@ describe('NaN Handling Tests', () => { }; // Act - const result = await Transaction.create(txData); + const result = await createTransaction(txData); // Assert: No transaction should be created and balance remains unchanged. expect(result).toBeUndefined(); diff --git a/api/models/User.js b/api/models/User.js deleted file mode 100644 index f4e8b0ec5..000000000 --- a/api/models/User.js +++ /dev/null @@ -1,6 +0,0 @@ -const mongoose = require('mongoose'); -const { userSchema } = require('@librechat/data-schemas'); - -const User = mongoose.model('User', userSchema); - -module.exports = User; diff --git a/api/models/balanceMethods.js b/api/models/balanceMethods.js index 4b788160a..7e6321ab2 100644 --- a/api/models/balanceMethods.js +++ b/api/models/balanceMethods.js @@ -1,9 +1,9 @@ +const { logger } = require('@librechat/data-schemas'); const { ViolationTypes } = require('librechat-data-provider'); -const { Transaction } = require('./Transaction'); +const { createAutoRefillTransaction } = require('./Transaction'); const { logViolation } = require('~/cache'); const { getMultiplier } = require('./tx'); -const { logger } = require('~/config'); -const Balance = require('./Balance'); +const { Balance } = require('~/db/models'); function isInvalidDate(date) { return isNaN(date); @@ -60,7 +60,7 @@ const checkBalanceRecord = async function ({ ) { try { /** @type {{ rate: number, user: string, balance: number, transaction: import('@librechat/data-schemas').ITransaction}} */ - const result = await Transaction.createAutoRefillTransaction({ + const result = await createAutoRefillTransaction({ user: user, tokenType: 'credits', context: 'autoRefill', diff --git a/api/models/convoStructure.spec.js b/api/models/convoStructure.spec.js index e672e0fa1..33bf0c9b2 100644 --- a/api/models/convoStructure.spec.js +++ b/api/models/convoStructure.spec.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); -const { Message, getMessages, bulkSaveMessages } = require('./Message'); +const { getMessages, bulkSaveMessages } = require('./Message'); +const { Message } = require('~/db/models'); // Original version of buildTree function function buildTree({ messages, fileMap }) { @@ -42,7 +43,6 @@ function buildTree({ messages, fileMap }) { } let mongod; - beforeAll(async () => { mongod = await MongoMemoryServer.create(); const uri = mongod.getUri(); diff --git a/api/models/index.js b/api/models/index.js index 73cfa1c96..7ecb9adcb 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,13 +1,7 @@ -const { - comparePassword, - deleteUserById, - generateToken, - getUserById, - updateUser, - createUser, - countUsers, - findUser, -} = require('./userMethods'); +const mongoose = require('mongoose'); +const { createMethods } = require('@librechat/data-schemas'); +const methods = createMethods(mongoose); +const { comparePassword } = require('./userMethods'); const { findFileById, createFile, @@ -26,32 +20,12 @@ const { deleteMessagesSince, deleteMessages, } = require('./Message'); -const { - createSession, - findSession, - updateExpiration, - deleteSession, - deleteAllUserSessions, - generateRefreshToken, - countActiveSessions, -} = require('./Session'); const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation'); const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset'); -const { createToken, findToken, updateToken, deleteTokens } = require('./Token'); -const Balance = require('./Balance'); -const User = require('./User'); -const Key = require('./Key'); module.exports = { + ...methods, comparePassword, - deleteUserById, - generateToken, - getUserById, - updateUser, - createUser, - countUsers, - findUser, - findFileById, createFile, updateFile, @@ -77,21 +51,4 @@ module.exports = { getPresets, savePreset, deletePresets, - - createToken, - findToken, - updateToken, - deleteTokens, - - createSession, - findSession, - updateExpiration, - deleteSession, - deleteAllUserSessions, - generateRefreshToken, - countActiveSessions, - - User, - Key, - Balance, }; diff --git a/api/models/inviteUser.js b/api/models/inviteUser.js index 6cd699fd6..9f35b3f02 100644 --- a/api/models/inviteUser.js +++ b/api/models/inviteUser.js @@ -1,7 +1,7 @@ const mongoose = require('mongoose'); -const { getRandomValues, hashToken } = require('~/server/utils/crypto'); -const { createToken, findToken } = require('./Token'); -const logger = require('~/config/winston'); +const { logger, hashToken } = require('@librechat/data-schemas'); +const { getRandomValues } = require('~/server/utils/crypto'); +const { createToken, findToken } = require('~/models'); /** * @module inviteUser diff --git a/api/models/schema/convoSchema.js b/api/models/schema/convoSchema.js deleted file mode 100644 index 89cb9c80b..000000000 --- a/api/models/schema/convoSchema.js +++ /dev/null @@ -1,18 +0,0 @@ -const mongoose = require('mongoose'); -const mongoMeili = require('../plugins/mongoMeili'); - -const { convoSchema } = require('@librechat/data-schemas'); - -if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { - convoSchema.plugin(mongoMeili, { - host: process.env.MEILI_HOST, - apiKey: process.env.MEILI_MASTER_KEY, - /** Note: Will get created automatically if it doesn't exist already */ - indexName: 'convos', - primaryKey: 'conversationId', - }); -} - -const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema); - -module.exports = Conversation; diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js deleted file mode 100644 index cf97b84ee..000000000 --- a/api/models/schema/messageSchema.js +++ /dev/null @@ -1,16 +0,0 @@ -const mongoose = require('mongoose'); -const mongoMeili = require('~/models/plugins/mongoMeili'); -const { messageSchema } = require('@librechat/data-schemas'); - -if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { - messageSchema.plugin(mongoMeili, { - host: process.env.MEILI_HOST, - apiKey: process.env.MEILI_MASTER_KEY, - indexName: 'messages', - primaryKey: 'messageId', - }); -} - -const Message = mongoose.models.Message || mongoose.model('Message', messageSchema); - -module.exports = Message; diff --git a/api/models/schema/pluginAuthSchema.js b/api/models/schema/pluginAuthSchema.js deleted file mode 100644 index 2066eda4c..000000000 --- a/api/models/schema/pluginAuthSchema.js +++ /dev/null @@ -1,6 +0,0 @@ -const mongoose = require('mongoose'); -const { pluginAuthSchema } = require('@librechat/data-schemas'); - -const PluginAuth = mongoose.models.Plugin || mongoose.model('PluginAuth', pluginAuthSchema); - -module.exports = PluginAuth; diff --git a/api/models/schema/presetSchema.js b/api/models/schema/presetSchema.js deleted file mode 100644 index 6d03803ac..000000000 --- a/api/models/schema/presetSchema.js +++ /dev/null @@ -1,6 +0,0 @@ -const mongoose = require('mongoose'); -const { presetSchema } = require('@librechat/data-schemas'); - -const Preset = mongoose.models.Preset || mongoose.model('Preset', presetSchema); - -module.exports = Preset; diff --git a/api/models/spendTokens.js b/api/models/spendTokens.js index 36b71ca9f..834f74092 100644 --- a/api/models/spendTokens.js +++ b/api/models/spendTokens.js @@ -1,6 +1,5 @@ -const { Transaction } = require('./Transaction'); const { logger } = require('~/config'); - +const { createTransaction, createStructuredTransaction } = require('./Transaction'); /** * Creates up to two transactions to record the spending of tokens. * @@ -33,7 +32,7 @@ const spendTokens = async (txData, tokenUsage) => { let prompt, completion; try { if (promptTokens !== undefined) { - prompt = await Transaction.create({ + prompt = await createTransaction({ ...txData, tokenType: 'prompt', rawAmount: promptTokens === 0 ? 0 : -Math.max(promptTokens, 0), @@ -41,7 +40,7 @@ const spendTokens = async (txData, tokenUsage) => { } if (completionTokens !== undefined) { - completion = await Transaction.create({ + completion = await createTransaction({ ...txData, tokenType: 'completion', rawAmount: completionTokens === 0 ? 0 : -Math.max(completionTokens, 0), @@ -101,7 +100,7 @@ const spendStructuredTokens = async (txData, tokenUsage) => { try { if (promptTokens) { const { input = 0, write = 0, read = 0 } = promptTokens; - prompt = await Transaction.createStructured({ + prompt = await createStructuredTransaction({ ...txData, tokenType: 'prompt', inputTokens: -input, @@ -111,7 +110,7 @@ const spendStructuredTokens = async (txData, tokenUsage) => { } if (completionTokens) { - completion = await Transaction.create({ + completion = await createTransaction({ ...txData, tokenType: 'completion', rawAmount: -completionTokens, diff --git a/api/models/spendTokens.spec.js b/api/models/spendTokens.spec.js index eacf42033..7ee067e58 100644 --- a/api/models/spendTokens.spec.js +++ b/api/models/spendTokens.spec.js @@ -1,8 +1,9 @@ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); -const { Transaction } = require('./Transaction'); -const Balance = require('./Balance'); const { spendTokens, spendStructuredTokens } = require('./spendTokens'); +const { createTransaction, createAutoRefillTransaction } = require('./Transaction'); + +require('~/db/models'); // Mock the logger to prevent console output during tests jest.mock('~/config', () => ({ @@ -19,11 +20,15 @@ jest.mock('~/server/services/Config'); describe('spendTokens', () => { let mongoServer; let userId; + let Transaction; + let Balance; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - await mongoose.connect(mongoUri); + await mongoose.connect(mongoServer.getUri()); + + Transaction = mongoose.model('Transaction'); + Balance = mongoose.model('Balance'); }); afterAll(async () => { @@ -197,7 +202,7 @@ describe('spendTokens', () => { // Check that the transaction records show the adjusted values const transactionResults = await Promise.all( transactions.map((t) => - Transaction.create({ + createTransaction({ ...txData, tokenType: t.tokenType, rawAmount: t.rawAmount, @@ -280,7 +285,7 @@ describe('spendTokens', () => { // Check the return values from Transaction.create directly // This is to verify that the incrementValue is not becoming positive - const directResult = await Transaction.create({ + const directResult = await createTransaction({ user: userId, conversationId: 'test-convo-3', model: 'gpt-4', @@ -607,7 +612,7 @@ describe('spendTokens', () => { const promises = []; for (let i = 0; i < numberOfRefills; i++) { promises.push( - Transaction.createAutoRefillTransaction({ + createAutoRefillTransaction({ user: userId, tokenType: 'credits', context: 'concurrent-refill-test', diff --git a/api/models/userMethods.js b/api/models/userMethods.js index fbcd33aba..e8bf5e478 100644 --- a/api/models/userMethods.js +++ b/api/models/userMethods.js @@ -1,159 +1,4 @@ const bcrypt = require('bcryptjs'); -const { getBalanceConfig } = require('~/server/services/Config'); -const signPayload = require('~/server/services/signPayload'); -const Balance = require('./Balance'); -const User = require('./User'); - -/** - * Retrieve a user by ID and convert the found user document to a plain object. - * - * @param {string} userId - The ID of the user to find and return as a plain object. - * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. - * @returns {Promise} A plain object representing the user document, or `null` if no user is found. - */ -const getUserById = async function (userId, fieldsToSelect = null) { - const query = User.findById(userId); - if (fieldsToSelect) { - query.select(fieldsToSelect); - } - return await query.lean(); -}; - -/** - * Search for a single user based on partial data and return matching user document as plain object. - * @param {Partial} searchCriteria - The partial data to use for searching the user. - * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. - * @returns {Promise} A plain object representing the user document, or `null` if no user is found. - */ -const findUser = async function (searchCriteria, fieldsToSelect = null) { - const query = User.findOne(searchCriteria); - if (fieldsToSelect) { - query.select(fieldsToSelect); - } - return await query.lean(); -}; - -/** - * Update a user with new data without overwriting existing properties. - * - * @param {string} userId - The ID of the user to update. - * @param {Object} updateData - An object containing the properties to update. - * @returns {Promise} The updated user document as a plain object, or `null` if no user is found. - */ -const updateUser = async function (userId, updateData) { - const updateOperation = { - $set: updateData, - $unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL - }; - return await User.findByIdAndUpdate(userId, updateOperation, { - new: true, - runValidators: true, - }).lean(); -}; - -/** - * Creates a new user, optionally with a TTL of 1 week. - * @param {MongoUser} data - The user data to be created, must contain user_id. - * @param {boolean} [disableTTL=true] - Whether to disable the TTL. Defaults to `true`. - * @param {boolean} [returnUser=false] - Whether to return the created user object. - * @returns {Promise} A promise that resolves to the created user document ID or user object. - * @throws {Error} If a user with the same user_id already exists. - */ -const createUser = async (data, disableTTL = true, returnUser = false) => { - const balance = await getBalanceConfig(); - const userData = { - ...data, - expiresAt: disableTTL ? null : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds - }; - - if (disableTTL) { - delete userData.expiresAt; - } - - const user = await User.create(userData); - - // If balance is enabled, create or update a balance record for the user using global.interfaceConfig.balance - if (balance?.enabled && balance?.startBalance) { - const update = { - $inc: { tokenCredits: balance.startBalance }, - }; - - if ( - balance.autoRefillEnabled && - balance.refillIntervalValue != null && - balance.refillIntervalUnit != null && - balance.refillAmount != null - ) { - update.$set = { - autoRefillEnabled: true, - refillIntervalValue: balance.refillIntervalValue, - refillIntervalUnit: balance.refillIntervalUnit, - refillAmount: balance.refillAmount, - }; - } - - await Balance.findOneAndUpdate({ user: user._id }, update, { upsert: true, new: true }).lean(); - } - - if (returnUser) { - return user.toObject(); - } - return user._id; -}; - -/** - * Count the number of user documents in the collection based on the provided filter. - * - * @param {Object} [filter={}] - The filter to apply when counting the documents. - * @returns {Promise} The count of documents that match the filter. - */ -const countUsers = async function (filter = {}) { - return await User.countDocuments(filter); -}; - -/** - * Delete a user by their unique ID. - * - * @param {string} userId - The ID of the user to delete. - * @returns {Promise<{ deletedCount: number }>} An object indicating the number of deleted documents. - */ -const deleteUserById = async function (userId) { - try { - const result = await User.deleteOne({ _id: userId }); - if (result.deletedCount === 0) { - return { deletedCount: 0, message: 'No user found with that ID.' }; - } - return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' }; - } catch (error) { - throw new Error('Error deleting user: ' + error.message); - } -}; - -const { SESSION_EXPIRY } = process.env ?? {}; -const expires = eval(SESSION_EXPIRY) ?? 1000 * 60 * 15; - -/** - * Generates a JWT token for a given user. - * - * @param {MongoUser} user - The user for whom the token is being generated. - * @returns {Promise} A promise that resolves to a JWT token. - */ -const generateToken = async (user) => { - if (!user) { - throw new Error('No user provided'); - } - - return await signPayload({ - payload: { - id: user._id, - username: user.username, - provider: user.provider, - email: user.email, - }, - secret: process.env.JWT_SECRET, - expirationTime: expires / 1000, - }); -}; /** * Compares the provided password with the user's password. @@ -179,11 +24,4 @@ const comparePassword = async (user, candidatePassword) => { module.exports = { comparePassword, - deleteUserById, - generateToken, - getUserById, - countUsers, - createUser, - updateUser, - findUser, }; diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index a71ce7d59..0f8152de3 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -1,6 +1,7 @@ -const openIdClient = require('openid-client'); const cookies = require('cookie'); const jwt = require('jsonwebtoken'); +const openIdClient = require('openid-client'); +const { logger } = require('@librechat/data-schemas'); const { registerUser, resetPassword, @@ -8,9 +9,8 @@ const { requestPasswordReset, setOpenIDAuthTokens, } = require('~/server/services/AuthService'); -const { findSession, getUserById, deleteAllUserSessions, findUser } = require('~/models'); +const { findUser, getUserById, deleteAllUserSessions, findSession } = require('~/models'); const { getOpenIdConfig } = require('~/strategies'); -const { logger } = require('~/config'); const { isEnabled } = require('~/server/utils'); const registrationController = async (req, res) => { @@ -96,7 +96,10 @@ const refreshController = async (req, res) => { } // Find the session with the hashed refresh token - const session = await findSession({ userId: userId, refreshToken: refreshToken }); + const session = await findSession({ + userId: userId, + refreshToken: refreshToken, + }); if (session && session.expiration > new Date()) { const token = await setAuthTokens(userId, res, session._id); diff --git a/api/server/controllers/Balance.js b/api/server/controllers/Balance.js index 0361045c7..c892a73b0 100644 --- a/api/server/controllers/Balance.js +++ b/api/server/controllers/Balance.js @@ -1,4 +1,4 @@ -const Balance = require('~/models/Balance'); +const { Balance } = require('~/db/models'); async function balanceController(req, res) { const balanceData = await Balance.findOne( diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index f5783f45a..6e22db2e5 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -1,12 +1,12 @@ +const { logger } = require('@librechat/data-schemas'); const { + verifyTOTP, + getTOTPSecret, + verifyBackupCode, generateTOTPSecret, generateBackupCodes, - verifyTOTP, - verifyBackupCode, - getTOTPSecret, } = require('~/server/services/twoFactorService'); -const { updateUser, getUserById } = require('~/models'); -const { logger } = require('~/config'); +const { getUserById, updateUser } = require('~/models'); const { encryptV3 } = require('~/server/utils/crypto'); const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 571c45455..a2fbc3c48 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -1,12 +1,11 @@ const { Tools, - Constants, FileSources, webSearchKeys, extractWebSearchEnvVars, } = require('librechat-data-provider'); +const { logger } = require('@librechat/data-schemas'); const { - Balance, getFiles, updateUser, deleteFiles, @@ -16,16 +15,14 @@ const { deleteUserById, deleteAllUserSessions, } = require('~/models'); -const User = require('~/models/User'); const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService'); const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud'); const { processDeleteRequest } = require('~/server/services/Files/process'); +const { Transaction, Balance, User } = require('~/db/models'); const { deleteAllSharedLinks } = require('~/models/Share'); const { deleteToolCalls } = require('~/models/ToolCall'); -const { Transaction } = require('~/models/Transaction'); -const { logger } = require('~/config'); const getUserController = async (req, res) => { /** @type {MongoUser} */ diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js index 15cde8122..b37c89a99 100644 --- a/api/server/controllers/auth/TwoFactorAuthController.js +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -1,12 +1,12 @@ const jwt = require('jsonwebtoken'); +const { logger } = require('@librechat/data-schemas'); const { verifyTOTP, - verifyBackupCode, getTOTPSecret, + verifyBackupCode, } = require('~/server/services/twoFactorService'); const { setAuthTokens } = require('~/server/services/AuthService'); -const { getUserById } = require('~/models/userMethods'); -const { logger } = require('~/config'); +const { getUserById } = require('~/models'); /** * Verifies the 2FA code during login using a temporary token. diff --git a/api/server/index.js b/api/server/index.js index c7525f9b9..ed770f770 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -9,8 +9,9 @@ const passport = require('passport'); const mongoSanitize = require('express-mongo-sanitize'); const fs = require('fs'); const cookieParser = require('cookie-parser'); +const { connectDb, indexSync } = require('~/db'); + const { jwtLogin, passportLogin } = require('~/strategies'); -const { connectDb, indexSync } = require('~/lib/db'); const { isEnabled } = require('~/server/utils'); const { ldapLogin } = require('~/strategies'); const { logger } = require('~/config'); @@ -36,6 +37,7 @@ const startServer = async () => { axios.defaults.headers.common['Accept-Encoding'] = 'gzip'; } await connectDb(); + logger.info('Connected to MongoDB'); await indexSync(); diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index 4e0593192..91c31ab66 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -1,12 +1,12 @@ const { Keyv } = require('keyv'); const uap = require('ua-parser-js'); +const { logger } = require('@librechat/data-schemas'); const { ViolationTypes } = require('librechat-data-provider'); const { isEnabled, removePorts } = require('~/server/utils'); const keyvMongo = require('~/cache/keyvMongo'); const denyRequest = require('./denyRequest'); const { getLogStores } = require('~/cache'); const { findUser } = require('~/models'); -const { logger } = require('~/config'); const banCache = new Keyv({ store: keyvMongo, namespace: ViolationTypes.BAN, ttl: 0 }); const message = 'Your account has been temporarily banned due to violations of our service.'; diff --git a/api/server/middleware/checkInviteUser.js b/api/server/middleware/checkInviteUser.js index e1ad271b5..42e1faba5 100644 --- a/api/server/middleware/checkInviteUser.js +++ b/api/server/middleware/checkInviteUser.js @@ -1,5 +1,5 @@ const { getInvite } = require('~/models/inviteUser'); -const { deleteTokens } = require('~/models/Token'); +const { deleteTokens } = require('~/models'); async function checkInviteUser(req, res, next) { const token = req.body.token; diff --git a/api/server/middleware/setBalanceConfig.js b/api/server/middleware/setBalanceConfig.js index 98d3cf114..5dd975796 100644 --- a/api/server/middleware/setBalanceConfig.js +++ b/api/server/middleware/setBalanceConfig.js @@ -1,6 +1,6 @@ +const { logger } = require('@librechat/data-schemas'); const { getBalanceConfig } = require('~/server/services/Config'); -const Balance = require('~/models/Balance'); -const { logger } = require('~/config'); +const { Balance } = require('~/db/models'); /** * Middleware to synchronize user balance settings with current balance configuration. diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index 7f771a482..356dd2509 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -1,4 +1,5 @@ const express = require('express'); +const { logger } = require('@librechat/data-schemas'); const { ContentTypes } = require('librechat-data-provider'); const { saveConvo, @@ -13,8 +14,7 @@ const { requireJwtAuth, validateMessageReq } = require('~/server/middleware'); const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc'); const { getConvosQueried } = require('~/models/Conversation'); const { countTokens } = require('~/server/utils'); -const { Message } = require('~/models/Message'); -const { logger } = require('~/config'); +const { Message } = require('~/db/models'); const router = express.Router(); router.use(requireJwtAuth); @@ -40,7 +40,11 @@ router.get('/', async (req, res) => { const sortOrder = sortDirection === 'asc' ? 1 : -1; if (conversationId && messageId) { - const message = await Message.findOne({ conversationId, messageId, user: user }).lean(); + const message = await Message.findOne({ + conversationId, + messageId, + user: user, + }).lean(); response = { messages: message ? [message] : [], nextCursor: null }; } else if (conversationId) { const filter = { conversationId, user: user }; diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index 740a77092..a35c74ad7 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -17,9 +17,9 @@ const { logger, getFlowStateManager, sendEvent } = require('~/config'); const { encryptV2, decryptV2 } = require('~/server/utils/crypto'); const { getActions, deleteActions } = require('~/models/Action'); const { deleteAssistant } = require('~/models/Assistant'); -const { findToken } = require('~/models/Token'); const { logAxiosError } = require('~/utils'); const { getLogStores } = require('~/cache'); +const { findToken } = require('~/models'); const JWT_SECRET = process.env.JWT_SECRET; const toolNameRegex = /^[a-zA-Z0-9_-]+$/; diff --git a/api/server/services/AppService.interface.spec.js b/api/server/services/AppService.interface.spec.js index 0bf9d07dc..90168d477 100644 --- a/api/server/services/AppService.interface.spec.js +++ b/api/server/services/AppService.interface.spec.js @@ -1,5 +1,7 @@ -jest.mock('~/models/Role', () => ({ +jest.mock('~/models', () => ({ initializeRoles: jest.fn(), +})); +jest.mock('~/models/Role', () => ({ updateAccessPermissions: jest.fn(), getRoleByName: jest.fn(), updateRoleByName: jest.fn(), diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 6a1cdfc69..4bb8c51d0 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -25,8 +25,8 @@ const { processModelSpecs } = require('./start/modelSpecs'); const { initializeS3 } = require('./Files/S3/initialize'); const { loadAndFormatTools } = require('./ToolService'); const { agentsConfigSetup } = require('./start/agents'); -const { initializeRoles } = require('~/models/Role'); const { isEnabled } = require('~/server/utils'); +const { initializeRoles } = require('~/models'); const { getMCPManager } = require('~/config'); const paths = require('~/config/paths'); diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index 0c7fac6ed..5e0837ce3 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -24,8 +24,10 @@ jest.mock('./Config/loadCustomConfig', () => { jest.mock('./Files/Firebase/initialize', () => ({ initializeFirebase: jest.fn(), })); -jest.mock('~/models/Role', () => ({ +jest.mock('~/models', () => ({ initializeRoles: jest.fn(), +})); +jest.mock('~/models/Role', () => ({ updateAccessPermissions: jest.fn(), })); jest.mock('./ToolService', () => ({ diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index ac1317212..11b37ac88 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -3,24 +3,23 @@ const { webcrypto } = require('node:crypto'); const { SystemRoles, errorsToString } = require('librechat-data-provider'); const { findUser, - countUsers, createUser, updateUser, - getUserById, - generateToken, - deleteUserById, -} = require('~/models/userMethods'); -const { - createToken, findToken, - deleteTokens, + countUsers, + getUserById, findSession, + createToken, + deleteTokens, deleteSession, createSession, + generateToken, + deleteUserById, generateRefreshToken, } = require('~/models'); const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils'); const { isEmailDomainAllowed } = require('~/server/services/domains'); +const { getBalanceConfig } = require('~/server/services/Config'); const { registerSchema } = require('~/strategies/validators'); const { logger } = require('~/config'); @@ -146,6 +145,7 @@ const verifyEmail = async (req) => { } const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true }); + if (!updatedUser) { logger.warn(`[verifyEmail] [User update failed] [Email: ${decodedEmail}]`); return new Error('Failed to update user verification status'); @@ -155,6 +155,7 @@ const verifyEmail = async (req) => { logger.info(`[verifyEmail] Email verification successful [Email: ${decodedEmail}]`); return { message: 'Email verification was successful', status: 'success' }; }; + /** * Register a new user. * @param {MongoUser} user @@ -216,7 +217,9 @@ const registerUser = async (user, additionalData = {}) => { const emailEnabled = checkEmailConfig(); const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN); - const newUser = await createUser(newUserData, disableTTL, true); + const balanceConfig = await getBalanceConfig(); + + const newUser = await createUser(newUserData, balanceConfig, disableTTL, true); newUserId = newUser._id; if (emailEnabled && !newUser.emailVerified) { await sendVerificationEmail({ @@ -389,6 +392,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => { throw error; } }; + /** * @function setOpenIDAuthTokens * Set OpenID Authentication Tokens diff --git a/api/server/services/Files/Azure/images.js b/api/server/services/Files/Azure/images.js index a83b700af..6672806db 100644 --- a/api/server/services/Files/Azure/images.js +++ b/api/server/services/Files/Azure/images.js @@ -1,10 +1,9 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); +const { logger } = require('@librechat/data-schemas'); const { resizeImageBuffer } = require('../images/resize'); -const { updateUser } = require('~/models/userMethods'); -const { updateFile } = require('~/models/File'); -const { logger } = require('~/config'); +const { updateUser, updateFile } = require('~/models'); const { saveBufferToAzure } = require('./crud'); /** diff --git a/api/server/services/Files/Firebase/images.js b/api/server/services/Files/Firebase/images.js index 7345f30df..980095da3 100644 --- a/api/server/services/Files/Firebase/images.js +++ b/api/server/services/Files/Firebase/images.js @@ -1,11 +1,10 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); +const { logger } = require('@librechat/data-schemas'); const { resizeImageBuffer } = require('../images/resize'); -const { updateUser } = require('~/models/userMethods'); +const { updateUser, updateFile } = require('~/models'); const { saveBufferToFirebase } = require('./crud'); -const { updateFile } = require('~/models/File'); -const { logger } = require('~/config'); /** * Converts an image file to the target format. The function first resizes the image based on the specified diff --git a/api/server/services/Files/Local/images.js b/api/server/services/Files/Local/images.js index 130550538..cb60d425c 100644 --- a/api/server/services/Files/Local/images.js +++ b/api/server/services/Files/Local/images.js @@ -2,8 +2,7 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const { resizeImageBuffer } = require('../images/resize'); -const { updateUser } = require('~/models/userMethods'); -const { updateFile } = require('~/models/File'); +const { updateUser, updateFile } = require('~/models'); /** * Converts an image file to the target format. The function first resizes the image based on the specified diff --git a/api/server/services/Files/S3/images.js b/api/server/services/Files/S3/images.js index 378212cb5..d8d9c85cf 100644 --- a/api/server/services/Files/S3/images.js +++ b/api/server/services/Files/S3/images.js @@ -1,11 +1,10 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); +const { logger } = require('@librechat/data-schemas'); const { resizeImageBuffer } = require('../images/resize'); -const { updateUser } = require('~/models/userMethods'); +const { updateUser, updateFile } = require('~/models'); const { saveBufferToS3 } = require('./crud'); -const { updateFile } = require('~/models/File'); -const { logger } = require('~/config'); const defaultBasePath = 'images'; diff --git a/api/server/services/PluginService.js b/api/server/services/PluginService.js index 03e90bce4..7463e0814 100644 --- a/api/server/services/PluginService.js +++ b/api/server/services/PluginService.js @@ -1,5 +1,5 @@ -const PluginAuth = require('~/models/schema/pluginAuthSchema'); -const { encrypt, decrypt } = require('~/server/utils/'); +const { encrypt, decrypt } = require('~/server/utils/crypto'); +const { PluginAuth } = require('~/db/models'); const { logger } = require('~/config'); /** diff --git a/api/server/services/UserService.js b/api/server/services/UserService.js index 91d772477..b729607f6 100644 --- a/api/server/services/UserService.js +++ b/api/server/services/UserService.js @@ -1,7 +1,8 @@ +const { logger } = require('@librechat/data-schemas'); const { ErrorTypes } = require('librechat-data-provider'); -const { encrypt, decrypt } = require('~/server/utils'); -const { updateUser, Key } = require('~/models'); -const { logger } = require('~/config'); +const { encrypt, decrypt } = require('~/server/utils/crypto'); +const { updateUser } = require('~/models'); +const { Key } = require('~/db/models'); /** * Updates the plugins for a user based on the action specified (install/uninstall). diff --git a/api/server/services/signPayload.js b/api/server/services/signPayload.js deleted file mode 100644 index a7bb0c64f..000000000 --- a/api/server/services/signPayload.js +++ /dev/null @@ -1,26 +0,0 @@ -const jwt = require('jsonwebtoken'); - -/** - * Signs a given payload using either the `jose` library (for Bun runtime) or `jsonwebtoken`. - * - * @async - * @function - * @param {Object} options - The options for signing the payload. - * @param {Object} options.payload - The payload to be signed. - * @param {string} options.secret - The secret key used for signing. - * @param {number} options.expirationTime - The expiration time in seconds. - * @returns {Promise} Returns a promise that resolves to the signed JWT. - * @throws {Error} Throws an error if there's an issue during signing. - * - * @example - * const signedPayload = await signPayload({ - * payload: { userId: 123 }, - * secret: 'my-secret-key', - * expirationTime: 3600 - * }); - */ -async function signPayload({ payload, secret, expirationTime }) { - return jwt.sign(payload, secret, { expiresIn: expirationTime }); -} - -module.exports = signPayload; diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js index d000c8fcf..027484236 100644 --- a/api/server/services/twoFactorService.js +++ b/api/server/services/twoFactorService.js @@ -1,6 +1,6 @@ const { webcrypto } = require('node:crypto'); -const { decryptV3, decryptV2 } = require('../utils/crypto'); -const { hashBackupCode } = require('~/server/utils/crypto'); +const { hashBackupCode, decryptV3, decryptV2 } = require('~/server/utils/crypto'); +const { updateUser } = require('~/models'); // Base32 alphabet for TOTP secret encoding. const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; @@ -172,7 +172,6 @@ const verifyBackupCode = async ({ user, backupCode }) => { : codeObj, ); // Update the user record with the marked backup code. - const { updateUser } = require('~/models'); await updateUser(user._id, { backupCodes: updatedBackupCodes }); return true; } diff --git a/api/server/utils/crypto.js b/api/server/utils/crypto.js index 333cd7573..2f176fede 100644 --- a/api/server/utils/crypto.js +++ b/api/server/utils/crypto.js @@ -106,12 +106,6 @@ function decryptV3(encryptedValue) { return decrypted.toString('utf8'); } -async function hashToken(str) { - const data = new TextEncoder().encode(str); - const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); - return Buffer.from(hashBuffer).toString('hex'); -} - async function getRandomValues(length) { if (!Number.isInteger(length) || length <= 0) { throw new Error('Length must be a positive integer'); @@ -141,7 +135,6 @@ module.exports = { decryptV2, encryptV3, decryptV3, - hashToken, hashBackupCode, getRandomValues, }; diff --git a/api/server/utils/handleText.spec.js b/api/server/utils/handleText.spec.js index 8b1b6eef8..2cd6c51f9 100644 --- a/api/server/utils/handleText.spec.js +++ b/api/server/utils/handleText.spec.js @@ -50,9 +50,13 @@ describe('isEnabled', () => { }); }); -jest.mock('crypto', () => ({ - randomBytes: jest.fn().mockReturnValue(Buffer.from('abc123', 'hex')), -})); +jest.mock('crypto', () => { + const actualModule = jest.requireActual('crypto'); + return { + ...actualModule, + randomBytes: jest.fn().mockReturnValue(Buffer.from('abc123', 'hex')), + }; +}); describe('sanitizeFilename', () => { test('removes directory components (1/2)', () => { diff --git a/api/strategies/appleStrategy.js b/api/strategies/appleStrategy.js index a45f10fc6..4dbac2e36 100644 --- a/api/strategies/appleStrategy.js +++ b/api/strategies/appleStrategy.js @@ -18,17 +18,13 @@ const getProfileDetails = ({ idToken, profile }) => { const decoded = jwt.decode(idToken); - logger.debug( - `Decoded Apple JWT: ${JSON.stringify(decoded, null, 2)}`, - ); + logger.debug(`Decoded Apple JWT: ${JSON.stringify(decoded, null, 2)}`); return { email: decoded.email, id: decoded.sub, avatarUrl: null, // Apple does not provide an avatar URL - username: decoded.email - ? decoded.email.split('@')[0].toLowerCase() - : `user_${decoded.sub}`, + username: decoded.email ? decoded.email.split('@')[0].toLowerCase() : `user_${decoded.sub}`, name: decoded.name ? `${decoded.name.firstName} ${decoded.name.lastName}` : profile.displayName || null, diff --git a/api/strategies/appleStrategy.test.js b/api/strategies/appleStrategy.test.js index c457e15fd..65a961bd4 100644 --- a/api/strategies/appleStrategy.test.js +++ b/api/strategies/appleStrategy.test.js @@ -1,22 +1,25 @@ -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); const jwt = require('jsonwebtoken'); +const mongoose = require('mongoose'); +const { logger } = require('@librechat/data-schemas'); const { Strategy: AppleStrategy } = require('passport-apple'); -const socialLogin = require('./socialLogin'); -const User = require('~/models/User'); -const { logger } = require('~/config'); +const { MongoMemoryServer } = require('mongodb-memory-server'); const { createSocialUser, handleExistingUser } = require('./process'); const { isEnabled } = require('~/server/utils'); +const socialLogin = require('./socialLogin'); const { findUser } = require('~/models'); +const { User } = require('~/db/models'); -// Mocking external dependencies jest.mock('jsonwebtoken'); -jest.mock('~/config', () => ({ - logger: { - error: jest.fn(), - debug: jest.fn(), - }, -})); +jest.mock('@librechat/data-schemas', () => { + const actualModule = jest.requireActual('@librechat/data-schemas'); + return { + ...actualModule, + logger: { + error: jest.fn(), + debug: jest.fn(), + }, + }; +}); jest.mock('./process', () => ({ createSocialUser: jest.fn(), handleExistingUser: jest.fn(), @@ -64,7 +67,6 @@ describe('Apple Login Strategy', () => { // Define getProfileDetails within the test scope getProfileDetails = ({ idToken, profile }) => { - console.log('getProfileDetails called with idToken:', idToken); if (!idToken) { logger.error('idToken is missing'); throw new Error('idToken is missing'); @@ -84,9 +86,7 @@ describe('Apple Login Strategy', () => { email: decoded.email, id: decoded.sub, avatarUrl: null, // Apple does not provide an avatar URL - username: decoded.email - ? decoded.email.split('@')[0].toLowerCase() - : `user_${decoded.sub}`, + username: decoded.email ? decoded.email.split('@')[0].toLowerCase() : `user_${decoded.sub}`, name: decoded.name ? `${decoded.name.firstName} ${decoded.name.lastName}` : profile.displayName || null, @@ -96,8 +96,12 @@ describe('Apple Login Strategy', () => { // Mock isEnabled based on environment variable isEnabled.mockImplementation((flag) => { - if (flag === 'true') { return true; } - if (flag === 'false') { return false; } + if (flag === 'true') { + return true; + } + if (flag === 'false') { + return false; + } return false; }); @@ -154,9 +158,7 @@ describe('Apple Login Strategy', () => { }); expect(jwt.decode).toHaveBeenCalledWith('fake_id_token'); - expect(logger.debug).toHaveBeenCalledWith( - expect.stringContaining('Decoded Apple JWT'), - ); + expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Decoded Apple JWT')); expect(profileDetails).toEqual({ email: 'john.doe@example.com', id: 'apple-sub-1234', @@ -209,7 +211,7 @@ describe('Apple Login Strategy', () => { beforeEach(() => { jwt.decode.mockReturnValue(decodedToken); - findUser.mockImplementation(({ email }) => User.findOne({ email })); + findUser.mockResolvedValue(null); }); it('should create a new user if one does not exist and registration is allowed', async () => { @@ -248,7 +250,7 @@ describe('Apple Login Strategy', () => { }); it('should handle existing user and update avatarUrl', async () => { - // Create an existing user + // Create an existing user without saving to database const existingUser = new User({ email: 'jane.doe@example.com', username: 'jane.doe', @@ -257,15 +259,15 @@ describe('Apple Login Strategy', () => { providerId: 'apple-sub-9012', avatarUrl: 'old_avatar.png', }); - await existingUser.save(); // Mock findUser to return the existing user findUser.mockResolvedValue(existingUser); - // Mock handleExistingUser to update avatarUrl + // Mock handleExistingUser to update avatarUrl without saving to database handleExistingUser.mockImplementation(async (user, avatarUrl) => { user.avatarUrl = avatarUrl; - await user.save(); + // Don't call save() to avoid database operations + return user; }); const mockVerifyCallback = jest.fn(); @@ -297,7 +299,7 @@ describe('Apple Login Strategy', () => { appleStrategyInstance._verify( fakeAccessToken, fakeRefreshToken, - null, // idToken is missing + null, // idToken is missing mockProfile, (err, user) => { mockVerifyCallback(err, user); diff --git a/api/strategies/jwtStrategy.js b/api/strategies/jwtStrategy.js index eb4b34fd8..6793873ee 100644 --- a/api/strategies/jwtStrategy.js +++ b/api/strategies/jwtStrategy.js @@ -1,7 +1,7 @@ +const { logger } = require('@librechat/data-schemas'); const { SystemRoles } = require('librechat-data-provider'); const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); const { getUserById, updateUser } = require('~/models'); -const { logger } = require('~/config'); // JWT strategy const jwtLogin = () => diff --git a/api/strategies/ldapStrategy.js b/api/strategies/ldapStrategy.js index beb9b8c2f..434534c26 100644 --- a/api/strategies/ldapStrategy.js +++ b/api/strategies/ldapStrategy.js @@ -1,10 +1,10 @@ const fs = require('fs'); const LdapStrategy = require('passport-ldapauth'); const { SystemRoles } = require('librechat-data-provider'); -const { findUser, createUser, updateUser } = require('~/models/userMethods'); -const { countUsers } = require('~/models/userMethods'); +const { logger } = require('@librechat/data-schemas'); +const { createUser, findUser, updateUser, countUsers } = require('~/models'); +const { getBalanceConfig } = require('~/server/services/Config'); const { isEnabled } = require('~/server/utils'); -const logger = require('~/utils/logger'); const { LDAP_URL, @@ -124,7 +124,8 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => { name: fullName, role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER, }; - const userId = await createUser(user); + const balanceConfig = await getBalanceConfig(); + const userId = await createUser(user, balanceConfig); user._id = userId; } else { // Users registered in LDAP are assumed to have their user information managed in LDAP, diff --git a/api/strategies/localStrategy.js b/api/strategies/localStrategy.js index bffb4f845..edc749ee9 100644 --- a/api/strategies/localStrategy.js +++ b/api/strategies/localStrategy.js @@ -1,9 +1,9 @@ +const { logger } = require('@librechat/data-schemas'); const { errorsToString } = require('librechat-data-provider'); const { Strategy: PassportLocalStrategy } = require('passport-local'); -const { findUser, comparePassword, updateUser } = require('~/models'); const { isEnabled, checkEmailConfig } = require('~/server/utils'); +const { findUser, comparePassword, updateUser } = require('~/models'); const { loginSchema } = require('./validators'); -const logger = require('~/utils/logger'); // Unix timestamp for 2024-06-07 15:20:18 Eastern Time const verificationEnabledTimestamp = 1717788018; diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index ea109358d..1d0a6bc5e 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -1,16 +1,16 @@ -const { CacheKeys } = require('librechat-data-provider'); const fetch = require('node-fetch'); const passport = require('passport'); -const jwtDecode = require('jsonwebtoken/decode'); -const { HttpsProxyAgent } = require('https-proxy-agent'); const client = require('openid-client'); +const jwtDecode = require('jsonwebtoken/decode'); +const { CacheKeys } = require('librechat-data-provider'); +const { HttpsProxyAgent } = require('https-proxy-agent'); +const { hashToken, logger } = require('@librechat/data-schemas'); const { Strategy: OpenIDStrategy } = require('openid-client/passport'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { findUser, createUser, updateUser } = require('~/models/userMethods'); -const { hashToken } = require('~/server/utils/crypto'); -const { isEnabled } = require('~/server/utils'); -const { logger } = require('~/config'); +const { findUser, createUser, updateUser } = require('~/models'); +const { getBalanceConfig } = require('~/server/services/Config'); const getLogStores = require('~/cache/getLogStores'); +const { isEnabled } = require('~/server/utils'); /** * @typedef {import('openid-client').ClientMetadata} ClientMetadata @@ -297,7 +297,10 @@ async function setupOpenId() { emailVerified: userinfo.email_verified || false, name: fullName, }; - user = await createUser(user, true, true); + + const balanceConfig = await getBalanceConfig(); + + user = await createUser(user, balanceConfig, true, true); } else { user.provider = 'openid'; user.openidId = userinfo.sub; diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index e70dfa552..3e52ad01f 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -1,7 +1,7 @@ const fetch = require('node-fetch'); const jwtDecode = require('jsonwebtoken/decode'); -const { findUser, createUser, updateUser } = require('~/models/userMethods'); const { setupOpenId } = require('./openidStrategy'); +const { findUser, createUser, updateUser } = require('~/models'); // --- Mocks --- jest.mock('node-fetch'); @@ -11,7 +11,12 @@ jest.mock('~/server/services/Files/strategies', () => ({ saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'), })), })); -jest.mock('~/models/userMethods', () => ({ +jest.mock('~/server/services/Config', () => ({ + getBalanceConfig: jest.fn(() => ({ + enabled: false, + })), +})); +jest.mock('~/models', () => ({ findUser: jest.fn(), createUser: jest.fn(), updateUser: jest.fn(), @@ -36,11 +41,6 @@ jest.mock('~/cache/getLogStores', () => set: jest.fn(), })), ); -jest.mock('librechat-data-provider', () => ({ - CacheKeys: { - OPENID_EXCHANGED_TOKENS: 'openid-exchanged-tokens', - }, -})); // Mock the openid-client module and all its dependencies jest.mock('openid-client', () => { @@ -174,6 +174,7 @@ describe('setupOpenId', () => { email: userinfo.email, name: `${userinfo.given_name} ${userinfo.family_name}`, }), + { enabled: false }, true, true, ); @@ -193,6 +194,7 @@ describe('setupOpenId', () => { expect(user.username).toBe(expectUsername); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ username: expectUsername }), + { enabled: false }, true, true, ); @@ -212,6 +214,7 @@ describe('setupOpenId', () => { expect(user.username).toBe(expectUsername); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ username: expectUsername }), + { enabled: false }, true, true, ); @@ -229,6 +232,7 @@ describe('setupOpenId', () => { expect(user.username).toBe(userinfo.sub); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ username: userinfo.sub }), + { enabled: false }, true, true, ); diff --git a/api/strategies/process.js b/api/strategies/process.js index e9a908ffd..774d8d015 100644 --- a/api/strategies/process.js +++ b/api/strategies/process.js @@ -1,7 +1,8 @@ const { FileSources } = require('librechat-data-provider'); -const { createUser, updateUser, getUserById } = require('~/models/userMethods'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); +const { updateUser, createUser, getUserById } = require('~/models'); +const { getBalanceConfig } = require('~/server/services/Config'); /** * Updates the avatar URL of an existing user. If the user's avatar URL does not include the query parameter @@ -78,7 +79,8 @@ const createSocialUser = async ({ emailVerified, }; - const newUserId = await createUser(update); + const balanceConfig = await getBalanceConfig(); + const newUserId = await createUser(update, balanceConfig); const fileStrategy = process.env.CDN_PROVIDER; const isLocal = fileStrategy === FileSources.local; diff --git a/api/strategies/samlStrategy.js b/api/strategies/samlStrategy.js index a0793f1c8..376434f73 100644 --- a/api/strategies/samlStrategy.js +++ b/api/strategies/samlStrategy.js @@ -2,11 +2,11 @@ const fs = require('fs'); const path = require('path'); const fetch = require('node-fetch'); const passport = require('passport'); +const { hashToken, logger } = require('@librechat/data-schemas'); const { Strategy: SamlStrategy } = require('@node-saml/passport-saml'); -const { findUser, createUser, updateUser } = require('~/models/userMethods'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { hashToken } = require('~/server/utils/crypto'); -const { logger } = require('~/config'); +const { findUser, createUser, updateUser } = require('~/models'); +const { getBalanceConfig } = require('~/server/services/Config'); const paths = require('~/config/paths'); let crypto; @@ -218,7 +218,8 @@ async function setupSaml() { emailVerified: true, name: fullName, }; - user = await createUser(user, true, true); + const balanceConfig = await getBalanceConfig(); + user = await createUser(user, balanceConfig, true, true); } else { user.provider = 'saml'; user.samlId = profile.nameID; diff --git a/api/strategies/samlStrategy.spec.js b/api/strategies/samlStrategy.spec.js index cb007c75e..675bdc998 100644 --- a/api/strategies/samlStrategy.spec.js +++ b/api/strategies/samlStrategy.spec.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const fetch = require('node-fetch'); const { Strategy: SamlStrategy } = require('@node-saml/passport-saml'); -const { findUser, createUser, updateUser } = require('~/models/userMethods'); +const { findUser, createUser, updateUser } = require('~/models'); const { setupSaml, getCertificateContent } = require('./samlStrategy'); // --- Mocks --- @@ -10,11 +10,29 @@ jest.mock('fs'); jest.mock('path'); jest.mock('node-fetch'); jest.mock('@node-saml/passport-saml'); -jest.mock('~/models/userMethods', () => ({ +jest.mock('~/models', () => ({ findUser: jest.fn(), createUser: jest.fn(), updateUser: jest.fn(), })); +jest.mock('~/server/services/Config', () => ({ + config: { + registration: { + socialLogins: ['saml'], + }, + }, + getBalanceConfig: jest.fn().mockResolvedValue({ + tokenCredits: 1000, + startingBalance: 1000, + }), +})); +jest.mock('~/server/services/Config/EndpointService', () => ({ + config: {}, +})); +jest.mock('~/server/utils', () => ({ + isEnabled: jest.fn(() => false), + isUserProvided: jest.fn(() => false), +})); jest.mock('~/server/services/Files/strategies', () => ({ getStrategyFunctions: jest.fn(() => ({ saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'), @@ -23,9 +41,6 @@ jest.mock('~/server/services/Files/strategies', () => ({ jest.mock('~/server/utils/crypto', () => ({ hashToken: jest.fn().mockResolvedValue('hashed-token'), })); -jest.mock('~/server/utils', () => ({ - isEnabled: jest.fn(() => false), -})); jest.mock('~/config', () => ({ logger: { info: jest.fn(), @@ -196,6 +211,18 @@ describe('setupSaml', () => { beforeEach(async () => { jest.clearAllMocks(); + // Configure mocks + const { findUser, createUser, updateUser } = require('~/models'); + findUser.mockResolvedValue(null); + createUser.mockImplementation(async (userData) => ({ + _id: 'mock-user-id', + ...userData, + })); + updateUser.mockImplementation(async (id, userData) => ({ + _id: id, + ...userData, + })); + const cert = ` -----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUKhXaFJGJJPx466rlwYORIsqCq7MwDQYJKoZIhvcNAQEL @@ -232,16 +259,6 @@ u7wlOSk+oFzDIO/UILIA delete process.env.SAML_PICTURE_CLAIM; delete process.env.SAML_NAME_CLAIM; - findUser.mockResolvedValue(null); - createUser.mockImplementation(async (userData) => ({ - _id: 'newUserId', - ...userData, - })); - updateUser.mockImplementation(async (id, userData) => ({ - _id: id, - ...userData, - })); - // Simulate image download const fakeBuffer = Buffer.from('fake image'); fetch.mockResolvedValue({ @@ -257,17 +274,10 @@ u7wlOSk+oFzDIO/UILIA const { user } = await validate(profile); expect(user.username).toBe(profile.username); - expect(createUser).toHaveBeenCalledWith( - expect.objectContaining({ - provider: 'saml', - samlId: profile.nameID, - username: profile.username, - email: profile.email, - name: `${profile.given_name} ${profile.family_name}`, - }), - true, - true, - ); + expect(user.provider).toBe('saml'); + expect(user.samlId).toBe(profile.nameID); + expect(user.email).toBe(profile.email); + expect(user.name).toBe(`${profile.given_name} ${profile.family_name}`); }); it('should use given_name as username when username claim is missing', async () => { @@ -278,11 +288,7 @@ u7wlOSk+oFzDIO/UILIA const { user } = await validate(profile); expect(user.username).toBe(expectUsername); - expect(createUser).toHaveBeenCalledWith( - expect.objectContaining({ username: expectUsername }), - true, - true, - ); + expect(user.provider).toBe('saml'); }); it('should use email as username when username and given_name are missing', async () => { @@ -294,11 +300,7 @@ u7wlOSk+oFzDIO/UILIA const { user } = await validate(profile); expect(user.username).toBe(expectUsername); - expect(createUser).toHaveBeenCalledWith( - expect.objectContaining({ username: expectUsername }), - true, - true, - ); + expect(user.provider).toBe('saml'); }); it('should override username with SAML_USERNAME_CLAIM when set', async () => { @@ -308,11 +310,7 @@ u7wlOSk+oFzDIO/UILIA const { user } = await validate(profile); expect(user.username).toBe(profile.nameID); - expect(createUser).toHaveBeenCalledWith( - expect.objectContaining({ username: profile.nameID }), - true, - true, - ); + expect(user.provider).toBe('saml'); }); it('should set the full name correctly when given_name and family_name exist', async () => { @@ -378,34 +376,26 @@ u7wlOSk+oFzDIO/UILIA }); it('should update an existing user on login', async () => { + // Set up findUser to return an existing user + const { findUser } = require('~/models'); const existingUser = { - _id: 'existingUserId', + _id: 'existing-user-id', provider: 'local', email: baseProfile.email, samlId: '', - username: '', - name: '', + username: 'oldusername', + name: 'Old Name', }; - - findUser.mockImplementation(async (query) => { - if (query.samlId === baseProfile.nameID || query.email === baseProfile.email) { - return existingUser; - } - return null; - }); + findUser.mockResolvedValue(existingUser); const profile = { ...baseProfile }; - await validate(profile); + const { user } = await validate(profile); - expect(updateUser).toHaveBeenCalledWith( - existingUser._id, - expect.objectContaining({ - provider: 'saml', - samlId: baseProfile.nameID, - username: baseProfile.username, - name: `${baseProfile.given_name} ${baseProfile.family_name}`, - }), - ); + expect(user.provider).toBe('saml'); + expect(user.samlId).toBe(baseProfile.nameID); + expect(user.username).toBe(baseProfile.username); + expect(user.name).toBe(`${baseProfile.given_name} ${baseProfile.family_name}`); + expect(user.email).toBe(baseProfile.email); }); it('should attempt to download and save the avatar if picture is provided', async () => { diff --git a/api/strategies/socialLogin.js b/api/strategies/socialLogin.js index 925c2de34..4f9462316 100644 --- a/api/strategies/socialLogin.js +++ b/api/strategies/socialLogin.js @@ -1,7 +1,7 @@ +const { logger } = require('@librechat/data-schemas'); const { createSocialUser, handleExistingUser } = require('./process'); const { isEnabled } = require('~/server/utils'); const { findUser } = require('~/models'); -const { logger } = require('~/config'); const socialLogin = (provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => { diff --git a/config/add-balance.js b/config/add-balance.js index fcd506382..d381fa029 100644 --- a/config/add-balance.js +++ b/config/add-balance.js @@ -1,9 +1,10 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { askQuestion, silentExit } = require('./helpers'); const { isEnabled } = require('~/server/utils/handleText'); -const { Transaction } = require('~/models/Transaction'); -const User = require('~/models/User'); +const { createTransaction } = require('~/models/Transaction'); const connect = require('./connect'); (async () => { @@ -78,7 +79,7 @@ const connect = require('./connect'); */ let result; try { - result = await Transaction.create({ + result = await createTransaction({ user: user._id, tokenType: 'credits', context: 'admin', diff --git a/config/ban-user.js b/config/ban-user.js index 89dc70c30..9612318ae 100644 --- a/config/ban-user.js +++ b/config/ban-user.js @@ -1,8 +1,9 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { askQuestion, silentExit } = require('./helpers'); const banViolation = require('~/cache/banViolation'); -const User = require('~/models/User'); const connect = require('./connect'); (async () => { diff --git a/config/connect.js b/config/connect.js index 00c5be857..6026c3e7b 100644 --- a/config/connect.js +++ b/config/connect.js @@ -5,7 +5,7 @@ const moduleAlias = require('module-alias'); const basePath = path.resolve(__dirname, '..', 'api'); moduleAlias.addAlias('~', basePath); -const connectDb = require('~/lib/db/connectDb'); +const { connectDb } = require('~/db/connect'); require('./helpers'); async function connect() { diff --git a/config/create-user.js b/config/create-user.js index 8c5429b53..5f8a39c6b 100644 --- a/config/create-user.js +++ b/config/create-user.js @@ -1,8 +1,9 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { registerUser } = require('~/server/services/AuthService'); const { askQuestion, silentExit } = require('./helpers'); -const User = require('~/models/User'); const connect = require('./connect'); (async () => { diff --git a/config/delete-user.js b/config/delete-user.js index 220cd2d18..36f34fb21 100644 --- a/config/delete-user.js +++ b/config/delete-user.js @@ -1,7 +1,8 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { askQuestion, silentExit } = require('./helpers'); -const User = require('~/models/User'); const connect = require('./connect'); (async () => { diff --git a/config/invite-user.js b/config/invite-user.js index 79925ec05..fde53eef9 100644 --- a/config/invite-user.js +++ b/config/invite-user.js @@ -1,9 +1,10 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { sendEmail, checkEmailConfig } = require('~/server/utils'); const { askQuestion, silentExit } = require('./helpers'); const { createInvite } = require('~/models/inviteUser'); -const User = require('~/models/User'); const connect = require('./connect'); (async () => { diff --git a/config/list-balances.js b/config/list-balances.js index 0878d19a5..1145559de 100644 --- a/config/list-balances.js +++ b/config/list-balances.js @@ -1,8 +1,8 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User, Balance } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { silentExit } = require('./helpers'); -const Balance = require('~/models/Balance'); -const User = require('~/models/User'); const connect = require('./connect'); (async () => { diff --git a/config/list-users.js b/config/list-users.js index bda6aff39..add7a58c0 100644 --- a/config/list-users.js +++ b/config/list-users.js @@ -1,6 +1,7 @@ const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); -const User = require('../api/models/User'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); const connect = require('./connect'); const listUsers = async () => { diff --git a/config/reset-terms.js b/config/reset-terms.js index 5dd621081..6bdacac52 100644 --- a/config/reset-terms.js +++ b/config/reset-terms.js @@ -1,8 +1,9 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); -const User = require('~/models/User'); -const connect = require('./connect'); const { askQuestion, silentExit } = require('./helpers'); +const connect = require('./connect'); (async () => { await connect(); diff --git a/config/set-balance.js b/config/set-balance.js index 6fb3b8ca9..d2c9adfef 100644 --- a/config/set-balance.js +++ b/config/set-balance.js @@ -1,10 +1,10 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +const { User, Balance } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { askQuestion, silentExit } = require('./helpers'); const { isEnabled } = require('~/server/utils/handleText'); -const User = require('~/models/User'); const connect = require('./connect'); -const Balance = require('~/models/Balance'); (async () => { await connect(); diff --git a/config/update-banner.js b/config/update-banner.js index 2f2d65bc2..7be3ef8a9 100644 --- a/config/update-banner.js +++ b/config/update-banner.js @@ -1,8 +1,9 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); const { v5: uuidv5 } = require('uuid'); +const { Banner } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { askQuestion, askMultiLineQuestion, silentExit } = require('./helpers'); -const { Banner } = require('~/models/Banner'); const connect = require('./connect'); (async () => { diff --git a/config/user-stats.js b/config/user-stats.js index 629469bdd..7716cf5c6 100644 --- a/config/user-stats.js +++ b/config/user-stats.js @@ -1,9 +1,8 @@ const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { silentExit } = require('./helpers'); -const Conversation = require('~/models/schema/convoSchema'); -const Message = require('~/models/schema/messageSchema'); -const User = require('~/models/User'); +const { User, Conversation, Message } = require('@librechat/data-schemas').createModels(mongoose); const connect = require('./connect'); (async () => { diff --git a/e2e/setup/cleanupUser.ts b/e2e/setup/cleanupUser.ts index fda3b45e9..01f59142e 100644 --- a/e2e/setup/cleanupUser.ts +++ b/e2e/setup/cleanupUser.ts @@ -1,12 +1,11 @@ -import connectDb from '@librechat/backend/lib/db/connectDb'; +import { connectDb } from '@librechat/backend/db/connect'; import { - deleteMessages, + findUser, deleteConvos, - User, + deleteMessages, deleteAllUserSessions, - Balance, } from '@librechat/backend/models'; -import { Transaction } from '@librechat/backend/models/Transaction'; + type TUser = { email: string; password: string }; export default async function cleanupUser(user: TUser) { @@ -16,28 +15,38 @@ export default async function cleanupUser(user: TUser) { const db = await connectDb(); console.log('🤖: ✅ Connected to Database'); - const { _id: user } = await User.findOne({ email }).lean(); + const foundUser = await findUser({ email }); + if (!foundUser) { + console.log('🤖: ⚠️ User not found in Database'); + return; + } + + const userId = foundUser._id; console.log('🤖: ✅ Found user in Database'); // Delete all conversations & associated messages - const { deletedCount, messages } = await deleteConvos(user, {}); + const { deletedCount, messages } = await deleteConvos(userId, {}); if (messages.deletedCount > 0 || deletedCount > 0) { console.log(`🤖: ✅ Deleted ${deletedCount} convos & ${messages.deletedCount} messages`); } // Ensure all user messages are deleted - const { deletedCount: deletedMessages } = await deleteMessages({ user }); + const { deletedCount: deletedMessages } = await deleteMessages({ user: userId }); if (deletedMessages > 0) { console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`); } - // TODO: fix this to delete all user sessions with the user's email - await deleteAllUserSessions(user); + // Delete all user sessions + await deleteAllUserSessions(userId.toString()); - await User.deleteMany({ _id: user }); - await Balance.deleteMany({ user }); - await Transaction.deleteMany({ user }); + // Get models from the registered models + const { User, Balance, Transaction } = getModels(); + + // Delete user, balance, and transactions using the registered models + await User.deleteMany({ _id: userId }); + await Balance.deleteMany({ user: userId }); + await Transaction.deleteMany({ user: userId }); console.log('🤖: ✅ Deleted user from Database'); diff --git a/package-lock.json b/package-lock.json index cec376673..298889740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2123,14 +2123,6 @@ "undici-types": "~5.26.4" } }, - "api/node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, "api/node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2140,14 +2132,6 @@ "node": ">= 14" } }, - "api/node_modules/bson": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", - "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", - "engines": { - "node": ">=16.20.1" - } - }, "api/node_modules/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -25022,6 +25006,12 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" }, + "node_modules/@types/traverse": { + "version": "0.6.37", + "resolved": "https://registry.npmjs.org/@types/traverse/-/traverse-0.6.37.tgz", + "integrity": "sha512-c90MVeDiUI1FhOZ6rLQ3kDWr50YE8+paDpM+5zbHjbmsqEp2DlMYkqnZnwbK9oI+NvDe8yRajup4jFwnVX6xsA==", + "dev": true + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -25048,6 +25038,14 @@ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/winston": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", @@ -25774,7 +25772,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -25922,7 +25919,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -26008,7 +26004,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -26078,7 +26073,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -26632,6 +26626,14 @@ "node-int64": "^0.4.0" } }, + "node_modules/bson": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -26737,7 +26739,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -27945,7 +27946,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -27963,7 +27963,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -27981,7 +27980,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -28150,7 +28148,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -28179,7 +28176,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -28703,7 +28699,6 @@ "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -28858,7 +28853,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -28883,7 +28877,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -30410,7 +30403,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -30600,7 +30592,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -30621,7 +30612,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -30806,7 +30796,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -30908,7 +30897,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -31131,7 +31119,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -31148,7 +31135,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -31160,7 +31146,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -31188,7 +31173,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -31943,7 +31927,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -32061,7 +32044,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -32085,7 +32067,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -32105,7 +32086,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -32132,7 +32112,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -32174,7 +32153,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -32201,7 +32179,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -32219,7 +32196,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -32268,7 +32244,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -32306,7 +32281,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -32359,7 +32333,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -32402,7 +32375,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -32468,7 +32440,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -32496,7 +32467,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -32509,7 +32479,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -32536,7 +32505,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -32553,7 +32521,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -32571,7 +32538,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -32587,7 +32553,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -32600,7 +32565,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -32616,7 +32580,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -35598,6 +35561,7 @@ "version": "0.38.0", "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.38.0.tgz", "integrity": "sha512-bHaq8nYxSKw9/Qslq1Zes5g9tHgFkxy/I9o8942wv2PqlNOT0CzptIkh/x98N52GikoSZOXSQkgt6oMjtf5uZw==", + "license": "MIT", "dependencies": { "cross-fetch": "^3.1.6" } @@ -36395,6 +36359,41 @@ "node": "*" } }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "peer": true, + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "peer": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -36915,7 +36914,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -36924,7 +36922,6 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -37167,7 +37164,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -37807,7 +37803,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -40062,7 +40057,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -40119,7 +40113,6 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -41193,7 +41186,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -41213,7 +41205,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/safe-buffer": { @@ -41239,7 +41230,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -41256,14 +41246,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -41427,7 +41415,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -41444,7 +41431,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -41460,7 +41446,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -42130,7 +42115,6 @@ "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -42152,7 +42136,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -42171,7 +42154,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -43172,7 +43154,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -43187,7 +43168,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -43207,7 +43187,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -43229,7 +43208,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -43251,6 +43229,28 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/typedarray.prototype.slice": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", + "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "math-intrinsics": "^1.1.0", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-offset": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -43341,7 +43341,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -44435,7 +44434,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -44455,7 +44453,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -44483,14 +44480,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -44509,7 +44504,6 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -45431,6 +45425,46 @@ "url": "https://github.com/sponsors/wooorm" } }, + "packages/auth": { + "name": "@librechat/auth", + "version": "0.0.1", + "extraneous": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^7.0.6", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.12.1", + "openid-client": "^6.5.0", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0" + }, + "devDependencies": { + "@librechat/data-schemas": "^0.0.7", + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.2", + "@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/diff": "^6.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "rimraf": "^5.0.1", + "rollup": "^4.22.4", + "rollup-plugin-generate-package-json": "^3.2.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-typescript2": "^0.35.0", + "ts-node": "^10.9.2", + "typescript": "^5.0.4" + }, + "peerDependencies": { + "keyv": "^5.3.2" + } + }, "packages/data-provider": { "name": "librechat-data-provider", "version": "0.7.86", @@ -45575,9 +45609,6 @@ "name": "@librechat/data-schemas", "version": "0.0.7", "license": "MIT", - "dependencies": { - "mongoose": "^8.12.1" - }, "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^25.0.2", @@ -45590,6 +45621,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.0", + "@types/traverse": "^0.6.37", "jest": "^29.5.0", "jest-junit": "^16.0.0", "rimraf": "^5.0.1", @@ -45601,15 +45633,16 @@ "typescript": "^5.0.4" }, "peerDependencies": { - "keyv": "^5.3.2" - } - }, - "packages/data-schemas/node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "dependencies": { - "@types/webidl-conversions": "*" + "jsonwebtoken": "^9.0.2", + "keyv": "^5.3.2", + "klona": "^2.0.6", + "librechat-data-provider": "*", + "lodash": "^4.17.21", + "meilisearch": "^0.38.0", + "mongoose": "^8.12.1", + "traverse": "^0.6.11", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" } }, "packages/data-schemas/node_modules/brace-expansion": { @@ -45622,14 +45655,6 @@ "balanced-match": "^1.0.0" } }, - "packages/data-schemas/node_modules/bson": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", - "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", - "engines": { - "node": ">=16.20.1" - } - }, "packages/data-schemas/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -45667,6 +45692,23 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "packages/data-schemas/node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "peer": true, + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "packages/data-schemas/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -45687,6 +45729,7 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", + "peer": true, "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.3", @@ -45728,19 +45771,11 @@ } } }, - "packages/data-schemas/node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, "packages/data-schemas/node_modules/mongoose": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", + "peer": true, "dependencies": { "bson": "^6.10.3", "kareem": "2.6.3", @@ -45758,6 +45793,29 @@ "url": "https://opencollective.com/mongoose" } }, + "packages/data-schemas/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "packages/data-schemas/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "packages/data-schemas/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -45774,27 +45832,75 @@ "url": "https://github.com/sponsors/isaacs" } }, - "packages/data-schemas/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "packages/data-schemas/node_modules/traverse": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", + "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", + "peer": true, "dependencies": { - "punycode": "^2.3.1" + "gopd": "^1.2.0", + "typedarray.prototype.slice": "^1.0.5", + "which-typed-array": "^1.1.18" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "packages/data-schemas/node_modules/whatwg-url": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", - "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "packages/data-schemas/node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "peer": true, "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" }, "engines": { - "node": ">=18" + "node": ">= 12.0.0" + } + }, + "packages/data-schemas/node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "peer": true, + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "packages/data-schemas/node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "peer": true, + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" } }, "packages/mcp": { diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 1a0f3fa15..9c2dddf0b 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -48,6 +48,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.0", + "@types/traverse": "^0.6.37", "jest": "^29.5.0", "jest-junit": "^16.0.0", "rimraf": "^5.0.1", @@ -58,11 +59,17 @@ "ts-node": "^10.9.2", "typescript": "^5.0.4" }, - "dependencies": { - "mongoose": "^8.12.1" - }, "peerDependencies": { - "keyv": "^5.3.2" + "keyv": "^5.3.2", + "mongoose": "^8.12.1", + "librechat-data-provider": "*", + "jsonwebtoken": "^9.0.2", + "klona": "^2.0.6", + "lodash": "^4.17.21", + "meilisearch": "^0.38.0", + "traverse": "^0.6.11", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" }, "publishConfig": { "registry": "https://registry.npmjs.org/", diff --git a/packages/data-schemas/src/config/meiliLogger.ts b/packages/data-schemas/src/config/meiliLogger.ts new file mode 100644 index 000000000..1341c328f --- /dev/null +++ b/packages/data-schemas/src/config/meiliLogger.ts @@ -0,0 +1,75 @@ +import path from 'path'; +import winston from 'winston'; +import 'winston-daily-rotate-file'; + +const logDir = path.join(__dirname, '..', 'logs'); + +const { NODE_ENV, DEBUG_LOGGING = 'false' } = process.env; + +const useDebugLogging = + (typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true') || + DEBUG_LOGGING === 'true'; + +const levels: winston.config.AbstractConfigSetLevels = { + error: 0, + warn: 1, + info: 2, + http: 3, + verbose: 4, + debug: 5, + activity: 6, + silly: 7, +}; + +winston.addColors({ + info: 'green', + warn: 'italic yellow', + error: 'red', + debug: 'blue', +}); + +const level = (): string => { + const env = NODE_ENV || 'development'; + const isDevelopment = env === 'development'; + return isDevelopment ? 'debug' : 'warn'; +}; + +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), +); + +const logLevel = useDebugLogging ? 'debug' : 'error'; +const transports: winston.transport[] = [ + new winston.transports.DailyRotateFile({ + level: logLevel, + filename: `${logDir}/meiliSync-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: fileFormat, + }), +]; + +const consoleFormat = winston.format.combine( + winston.format.colorize({ all: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), +); + +transports.push( + new winston.transports.Console({ + level: 'info', + format: consoleFormat, + }), +); + +const logger = winston.createLogger({ + level: level(), + levels, + transports, +}); + +export default logger; diff --git a/packages/data-schemas/src/config/parsers.ts b/packages/data-schemas/src/config/parsers.ts new file mode 100644 index 000000000..306484481 --- /dev/null +++ b/packages/data-schemas/src/config/parsers.ts @@ -0,0 +1,241 @@ +import { klona } from 'klona'; +import winston from 'winston'; +import traverse from 'traverse'; + +const SPLAT_SYMBOL = Symbol.for('splat'); +const MESSAGE_SYMBOL = Symbol.for('message'); +const CONSOLE_JSON_STRING_LENGTH: number = + parseInt(process.env.CONSOLE_JSON_STRING_LENGTH || '', 10) || 255; + +const sensitiveKeys: RegExp[] = [ + /^(sk-)[^\s]+/, // OpenAI API key pattern + /(Bearer )[^\s]+/, // Header: Bearer token pattern + /(api-key:? )[^\s]+/, // Header: API key pattern + /(key=)[^\s]+/, // URL query param: sensitive key pattern (Google) +]; + +/** + * Determines if a given value string is sensitive and returns matching regex patterns. + * + * @param valueStr - The value string to check. + * @returns An array of regex patterns that match the value string. + */ +function getMatchingSensitivePatterns(valueStr: string): RegExp[] { + if (valueStr) { + // Filter and return all regex patterns that match the value string + return sensitiveKeys.filter((regex) => regex.test(valueStr)); + } + return []; +} + +/** + * Redacts sensitive information from a console message and trims it to a specified length if provided. + * @param str - The console message to be redacted. + * @param trimLength - The optional length at which to trim the redacted message. + * @returns The redacted and optionally trimmed console message. + */ +function redactMessage(str: string, trimLength?: number): string { + if (!str) { + return ''; + } + + const patterns = getMatchingSensitivePatterns(str); + patterns.forEach((pattern) => { + str = str.replace(pattern, '$1[REDACTED]'); + }); + + if (trimLength !== undefined && str.length > trimLength) { + return `${str.substring(0, trimLength)}...`; + } + + return str; +} + +/** + * Redacts sensitive information from log messages if the log level is 'error'. + * Note: Intentionally mutates the object. + * @param info - The log information object. + * @returns The modified log information object. + */ +const redactFormat = winston.format((info: winston.Logform.TransformableInfo) => { + if (info.level === 'error') { + // Type guard to ensure message is a string + if (typeof info.message === 'string') { + info.message = redactMessage(info.message); + } + + // Handle MESSAGE_SYMBOL with type safety + const symbolValue = (info as Record)[MESSAGE_SYMBOL]; + if (typeof symbolValue === 'string') { + (info as Record)[MESSAGE_SYMBOL] = redactMessage(symbolValue); + } + } + return info; +}); + +/** + * Truncates long strings, especially base64 image data, within log messages. + * + * @param value - The value to be inspected and potentially truncated. + * @param length - The length at which to truncate the value. Default: 100. + * @returns The truncated or original value. + */ +const truncateLongStrings = (value: unknown, length = 100): unknown => { + if (typeof value === 'string') { + return value.length > length ? value.substring(0, length) + '... [truncated]' : value; + } + + return value; +}; + +/** + * An array mapping function that truncates long strings (objects converted to JSON strings). + * @param item - The item to be condensed. + * @returns The condensed item. + */ +const condenseArray = (item: unknown): string | unknown => { + if (typeof item === 'string') { + return truncateLongStrings(JSON.stringify(item)); + } else if (typeof item === 'object') { + return truncateLongStrings(JSON.stringify(item)); + } + return item; +}; + +/** + * Formats log messages for debugging purposes. + * - Truncates long strings within log messages. + * - Condenses arrays by truncating long strings and objects as strings within array items. + * - Redacts sensitive information from log messages if the log level is 'error'. + * - Converts log information object to a formatted string. + * + * @param options - The options for formatting log messages. + * @returns The formatted log message. + */ +const debugTraverse = winston.format.printf( + ({ level, message, timestamp, ...metadata }: Record) => { + if (!message) { + return `${timestamp} ${level}`; + } + + // Type-safe version of the CJS logic: !message?.trim || typeof message !== 'string' + if (typeof message !== 'string' || !message.trim) { + return `${timestamp} ${level}: ${JSON.stringify(message)}`; + } + + let msg = `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`; + + try { + if (level !== 'debug') { + return msg; + } + + if (!metadata) { + return msg; + } + + // Type-safe access to SPLAT_SYMBOL using bracket notation + const metadataRecord = metadata as Record; + const splatArray = metadataRecord[SPLAT_SYMBOL]; + const debugValue = Array.isArray(splatArray) ? splatArray[0] : undefined; + + if (!debugValue) { + return msg; + } + + if (debugValue && Array.isArray(debugValue)) { + msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`; + return msg; + } + + if (typeof debugValue !== 'object') { + return (msg += ` ${debugValue}`); + } + + msg += '\n{'; + + const copy = klona(metadata); + + traverse(copy).forEach(function (this: traverse.TraverseContext, value: unknown) { + if (typeof this?.key === 'symbol') { + return; + } + + let _parentKey = ''; + const parent = this.parent; + + if (typeof parent?.key !== 'symbol' && parent?.key) { + _parentKey = parent.key; + } + + const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`; + const tabs = `${parent && parent.notRoot ? ' ' : ' '}`; + const currentKey = this?.key ?? 'unknown'; + + if (this.isLeaf && typeof value === 'string') { + const truncatedText = truncateLongStrings(value); + msg += `\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`; + } else if (this.notLeaf && Array.isArray(value) && value.length > 0) { + const currentMessage = `\n${tabs}// ${value.length} ${currentKey.replace(/s$/, '')}(s)`; + this.update(currentMessage, true); + msg += currentMessage; + const stringifiedArray = value.map(condenseArray); + msg += `\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`; + } else if (this.isLeaf && typeof value === 'function') { + msg += `\n${tabs}${parentKey}${currentKey}: function,`; + } else if (this.isLeaf) { + msg += `\n${tabs}${parentKey}${currentKey}: ${value},`; + } + }); + + msg += '\n}'; + return msg; + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error'; + return (msg += `\n[LOGGER PARSING ERROR] ${errorMessage}`); + } + }, +); + +/** + * Truncates long string values in JSON log objects. + * Prevents outputting extremely long values (e.g., base64, blobs). + */ +const jsonTruncateFormat = winston.format((info: winston.Logform.TransformableInfo) => { + const truncateLongStrings = (str: string, maxLength: number): string => + str.length > maxLength ? str.substring(0, maxLength) + '...' : str; + + const seen = new WeakSet(); + + const truncateObject = (obj: unknown): unknown => { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + // Handle circular references - now with proper object type + if (seen.has(obj)) { + return '[Circular]'; + } + seen.add(obj); + + if (Array.isArray(obj)) { + return obj.map((item) => truncateObject(item)); + } + + // We know this is an object at this point + const objectRecord = obj as Record; + const newObj: Record = {}; + Object.entries(objectRecord).forEach(([key, value]) => { + if (typeof value === 'string') { + newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH); + } else { + newObj[key] = truncateObject(value); + } + }); + return newObj; + }; + + return truncateObject(info) as winston.Logform.TransformableInfo; +}); + +export { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat }; diff --git a/packages/data-schemas/src/config/winston.ts b/packages/data-schemas/src/config/winston.ts new file mode 100644 index 000000000..598d96739 --- /dev/null +++ b/packages/data-schemas/src/config/winston.ts @@ -0,0 +1,123 @@ +import path from 'path'; +import winston from 'winston'; +import 'winston-daily-rotate-file'; +import { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat } from './parsers'; + +// Define log directory +const logDir = path.join(__dirname, '..', 'logs'); + +// Type-safe environment variables +const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE } = process.env; + +const useConsoleJson = typeof CONSOLE_JSON === 'string' && CONSOLE_JSON.toLowerCase() === 'true'; + +const useDebugConsole = typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE.toLowerCase() === 'true'; + +const useDebugLogging = typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true'; + +// Define custom log levels +const levels: winston.config.AbstractConfigSetLevels = { + error: 0, + warn: 1, + info: 2, + http: 3, + verbose: 4, + debug: 5, + activity: 6, + silly: 7, +}; + +winston.addColors({ + info: 'green', + warn: 'italic yellow', + error: 'red', + debug: 'blue', +}); + +const level = (): string => { + const env = NODE_ENV || 'development'; + return env === 'development' ? 'debug' : 'warn'; +}; + +const fileFormat = winston.format.combine( + redactFormat(), + winston.format.timestamp({ format: () => new Date().toISOString() }), + winston.format.errors({ stack: true }), + winston.format.splat(), +); + +const transports: winston.transport[] = [ + new winston.transports.DailyRotateFile({ + level: 'error', + filename: `${logDir}/error-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: fileFormat, + }), +]; + +if (useDebugLogging) { + transports.push( + new winston.transports.DailyRotateFile({ + level: 'debug', + filename: `${logDir}/debug-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: winston.format.combine(fileFormat, debugTraverse), + }), + ); +} + +const consoleFormat = winston.format.combine( + redactFormat(), + winston.format.colorize({ all: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.printf((info) => { + const message = `${info.timestamp} ${info.level}: ${info.message}`; + return info.level.includes('error') ? redactMessage(message) : message; + }), +); + +let consoleLogLevel: string = 'info'; +if (useDebugConsole) { + consoleLogLevel = 'debug'; +} + +// Add console transport +if (useDebugConsole) { + transports.push( + new winston.transports.Console({ + level: consoleLogLevel, + format: useConsoleJson + ? winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()) + : winston.format.combine(fileFormat, debugTraverse), + }), + ); +} else if (useConsoleJson) { + transports.push( + new winston.transports.Console({ + level: consoleLogLevel, + format: winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()), + }), + ); +} else { + transports.push( + new winston.transports.Console({ + level: consoleLogLevel, + format: consoleFormat, + }), + ); +} + +// Create logger +const logger = winston.createLogger({ + level: level(), + levels, + transports, +}); + +export default logger; diff --git a/packages/data-schemas/src/crypto/index.ts b/packages/data-schemas/src/crypto/index.ts new file mode 100644 index 000000000..b9118896f --- /dev/null +++ b/packages/data-schemas/src/crypto/index.ts @@ -0,0 +1,17 @@ +import jwt from 'jsonwebtoken'; +import { webcrypto } from 'node:crypto'; +import { SignPayloadParams } from '~/types'; + +export async function signPayload({ + payload, + secret, + expirationTime, +}: SignPayloadParams): Promise { + return jwt.sign(payload, secret!, { expiresIn: expirationTime }); +} + +export async function hashToken(str: string): Promise { + const data = new TextEncoder().encode(str); + const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); + return Buffer.from(hashBuffer).toString('hex'); +} diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index 4b3af06b8..a5a4ec59e 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -1,68 +1,7 @@ -export { default as actionSchema } from './schema/action'; -export type { IAction } from './schema/action'; - -export { default as agentSchema } from './schema/agent'; -export type { IAgent } from './schema/agent'; - -export { default as assistantSchema } from './schema/assistant'; -export type { IAssistant } from './schema/assistant'; - -export { default as balanceSchema } from './schema/balance'; -export type { IBalance } from './schema/balance'; - -export { default as bannerSchema } from './schema/banner'; -export type { IBanner } from './schema/banner'; - -export { default as categoriesSchema } from './schema/categories'; -export type { ICategory } from './schema/categories'; - -export { default as conversationTagSchema } from './schema/conversationTag'; -export type { IConversationTag } from './schema/conversationTag'; - -export { default as convoSchema } from './schema/convo'; -export type { IConversation } from './schema/convo'; - -export { default as fileSchema } from './schema/file'; -export type { IMongoFile } from './schema/file'; - -export { default as keySchema } from './schema/key'; -export type { IKey } from './schema/key'; - -export { default as messageSchema } from './schema/message'; -export type { IMessage } from './schema/message'; - -export { default as pluginAuthSchema } from './schema/pluginAuth'; -export type { IPluginAuth } from './schema/pluginAuth'; - -export { default as presetSchema } from './schema/preset'; -export type { IPreset } from './schema/preset'; - -export { default as projectSchema } from './schema/project'; -export type { IMongoProject } from './schema/project'; - -export { default as promptSchema } from './schema/prompt'; -export type { IPrompt } from './schema/prompt'; - -export { default as promptGroupSchema } from './schema/promptGroup'; -export type { IPromptGroup, IPromptGroupDocument } from './schema/promptGroup'; - -export { default as roleSchema } from './schema/role'; -export type { IRole } from './schema/role'; - -export { default as sessionSchema } from './schema/session'; -export type { ISession } from './schema/session'; - -export { default as shareSchema } from './schema/share'; -export type { ISharedLink } from './schema/share'; - -export { default as tokenSchema } from './schema/token'; -export type { IToken } from './schema/token'; - -export { default as toolCallSchema } from './schema/toolCall'; -export type { IToolCallData } from './schema/toolCall'; - -export { default as transactionSchema } from './schema/transaction'; -export type { ITransaction } from './schema/transaction'; - -export { default as userSchema } from './schema/user'; -export type { IUser } from './schema/user'; +export * from './crypto'; +export * from './schema'; +export { createModels } from './models'; +export { createMethods } from './methods'; +export type * from './types'; +export { default as logger } from './config/winston'; +export { default as meiliLogger } from './config/meiliLogger'; diff --git a/packages/data-schemas/src/methods/index.ts b/packages/data-schemas/src/methods/index.ts new file mode 100644 index 000000000..324159b0a --- /dev/null +++ b/packages/data-schemas/src/methods/index.ts @@ -0,0 +1,18 @@ +import { createUserMethods, type UserMethods } from './user'; +import { createSessionMethods, type SessionMethods } from './session'; +import { createTokenMethods, type TokenMethods } from './token'; +import { createRoleMethods, type RoleMethods } from './role'; + +/** + * Creates all database methods for all collections + */ +export function createMethods(mongoose: typeof import('mongoose')) { + return { + ...createUserMethods(mongoose), + ...createSessionMethods(mongoose), + ...createTokenMethods(mongoose), + ...createRoleMethods(mongoose), + }; +} + +export type AllMethods = UserMethods & SessionMethods & TokenMethods & RoleMethods; diff --git a/packages/data-schemas/src/methods/role.ts b/packages/data-schemas/src/methods/role.ts new file mode 100644 index 000000000..208636f7f --- /dev/null +++ b/packages/data-schemas/src/methods/role.ts @@ -0,0 +1,50 @@ +import { roleDefaults, SystemRoles } from 'librechat-data-provider'; + +// Factory function that takes mongoose instance and returns the methods +export function createRoleMethods(mongoose: typeof import('mongoose')) { + /** + * Initialize default roles in the system. + * Creates the default roles (ADMIN, USER) if they don't exist in the database. + * Updates existing roles with new permission types if they're missing. + */ + async function initializeRoles() { + const Role = mongoose.models.Role; + + for (const roleName of [SystemRoles.ADMIN, SystemRoles.USER]) { + let role = await Role.findOne({ name: roleName }); + const defaultPerms = roleDefaults[roleName].permissions; + + if (!role) { + // Create new role if it doesn't exist. + role = new Role(roleDefaults[roleName]); + } else { + // Ensure role.permissions is defined. + role.permissions = role.permissions || {}; + // For each permission type in defaults, add it if missing. + for (const permType of Object.keys(defaultPerms)) { + if (role.permissions[permType] == null) { + role.permissions[permType] = defaultPerms[permType as keyof typeof defaultPerms]; + } + } + } + await role.save(); + } + } + + /** + * List all roles in the system (for testing purposes) + * Returns an array of all roles with their names and permissions + */ + async function listRoles() { + const Role = mongoose.models.Role; + return await Role.find({}).select('name permissions').lean(); + } + + // Return all methods you want to expose + return { + listRoles, + initializeRoles, + }; +} + +export type RoleMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/session.ts b/packages/data-schemas/src/methods/session.ts new file mode 100644 index 000000000..8c44aa54d --- /dev/null +++ b/packages/data-schemas/src/methods/session.ts @@ -0,0 +1,264 @@ +import type * as t from '~/types/session'; +import { signPayload, hashToken } from '~/crypto'; +import logger from '~/config/winston'; + +export class SessionError extends Error { + public code: string; + + constructor(message: string, code: string = 'SESSION_ERROR') { + super(message); + this.name = 'SessionError'; + this.code = code; + } +} + +const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; +const expires = eval(REFRESH_TOKEN_EXPIRY ?? '0') ?? 1000 * 60 * 60 * 24 * 7; // 7 days default + +// Factory function that takes mongoose instance and returns the methods +export function createSessionMethods(mongoose: typeof import('mongoose')) { + const Session = mongoose.models.Session; + + /** + * Creates a new session for a user + */ + async function createSession( + userId: string, + options: t.CreateSessionOptions = {}, + ): Promise { + if (!userId) { + throw new SessionError('User ID is required', 'INVALID_USER_ID'); + } + + try { + const session = new Session({ + user: userId, + expiration: options.expiration || new Date(Date.now() + expires), + }); + const refreshToken = await generateRefreshToken(session); + + return { session, refreshToken }; + } catch (error) { + logger.error('[createSession] Error creating session:', error); + throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED'); + } + } + + /** + * Finds a session by various parameters + */ + async function findSession( + params: t.SessionSearchParams, + options: t.SessionQueryOptions = { lean: true }, + ): Promise { + try { + const query: Record = {}; + + if (!params.refreshToken && !params.userId && !params.sessionId) { + throw new SessionError( + 'At least one search parameter is required', + 'INVALID_SEARCH_PARAMS', + ); + } + + if (params.refreshToken) { + const tokenHash = await hashToken(params.refreshToken); + query.refreshTokenHash = tokenHash; + } + + if (params.userId) { + query.user = params.userId; + } + + if (params.sessionId) { + const sessionId = + typeof params.sessionId === 'object' && + params.sessionId !== null && + 'sessionId' in params.sessionId + ? (params.sessionId as { sessionId: string }).sessionId + : (params.sessionId as string); + if (!mongoose.Types.ObjectId.isValid(sessionId)) { + throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID'); + } + query._id = sessionId; + } + + // Add expiration check to only return valid sessions + query.expiration = { $gt: new Date() }; + + const sessionQuery = Session.findOne(query); + + if (options.lean) { + return (await sessionQuery.lean()) as t.ISession | null; + } + + return await sessionQuery.exec(); + } catch (error) { + logger.error('[findSession] Error finding session:', error); + throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED'); + } + } + + /** + * Updates session expiration + */ + async function updateExpiration( + session: t.ISession | string, + newExpiration?: Date, + ): Promise { + try { + const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session; + + if (!sessionDoc) { + throw new SessionError('Session not found', 'SESSION_NOT_FOUND'); + } + + sessionDoc.expiration = newExpiration || new Date(Date.now() + expires); + return await sessionDoc.save(); + } catch (error) { + logger.error('[updateExpiration] Error updating session:', error); + throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED'); + } + } + + /** + * Deletes a session by refresh token or session ID + */ + async function deleteSession(params: t.DeleteSessionParams): Promise<{ deletedCount?: number }> { + try { + if (!params.refreshToken && !params.sessionId) { + throw new SessionError( + 'Either refreshToken or sessionId is required', + 'INVALID_DELETE_PARAMS', + ); + } + + const query: Record = {}; + + if (params.refreshToken) { + query.refreshTokenHash = await hashToken(params.refreshToken); + } + + if (params.sessionId) { + query._id = params.sessionId; + } + + const result = await Session.deleteOne(query); + + if (result.deletedCount === 0) { + logger.warn('[deleteSession] No session found to delete'); + } + + return result; + } catch (error) { + logger.error('[deleteSession] Error deleting session:', error); + throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED'); + } + } + + /** + * Deletes all sessions for a user + */ + async function deleteAllUserSessions( + userId: string | { userId: string }, + options: t.DeleteAllSessionsOptions = {}, + ): Promise<{ deletedCount?: number }> { + try { + if (!userId) { + throw new SessionError('User ID is required', 'INVALID_USER_ID'); + } + + const userIdString = + typeof userId === 'object' && userId !== null ? userId.userId : (userId as string); + + if (!mongoose.Types.ObjectId.isValid(userIdString)) { + throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT'); + } + + const query: Record = { user: userIdString }; + + if (options.excludeCurrentSession && options.currentSessionId) { + query._id = { $ne: options.currentSessionId }; + } + + const result = await Session.deleteMany(query); + + if (result.deletedCount && result.deletedCount > 0) { + logger.debug( + `[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`, + ); + } + + return result; + } catch (error) { + logger.error('[deleteAllUserSessions] Error deleting user sessions:', error); + throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED'); + } + } + + /** + * Generates a refresh token for a session + */ + async function generateRefreshToken(session: t.ISession): Promise { + if (!session || !session.user) { + throw new SessionError('Invalid session object', 'INVALID_SESSION'); + } + + try { + const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires; + + if (!session.expiration) { + session.expiration = new Date(expiresIn); + } + + const refreshToken = await signPayload({ + payload: { + id: session.user, + sessionId: session._id, + }, + secret: process.env.JWT_REFRESH_SECRET!, + expirationTime: Math.floor((expiresIn - Date.now()) / 1000), + }); + + session.refreshTokenHash = await hashToken(refreshToken); + await session.save(); + + return refreshToken; + } catch (error) { + logger.error('[generateRefreshToken] Error generating refresh token:', error); + throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED'); + } + } + + /** + * Counts active sessions for a user + */ + async function countActiveSessions(userId: string): Promise { + try { + if (!userId) { + throw new SessionError('User ID is required', 'INVALID_USER_ID'); + } + + return await Session.countDocuments({ + user: userId, + expiration: { $gt: new Date() }, + }); + } catch (error) { + logger.error('[countActiveSessions] Error counting active sessions:', error); + throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED'); + } + } + + return { + findSession, + SessionError, + deleteSession, + createSession, + updateExpiration, + countActiveSessions, + generateRefreshToken, + deleteAllUserSessions, + }; +} + +export type SessionMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/token.ts b/packages/data-schemas/src/methods/token.ts new file mode 100644 index 000000000..2c23ddec3 --- /dev/null +++ b/packages/data-schemas/src/methods/token.ts @@ -0,0 +1,105 @@ +import { IToken, TokenCreateData, TokenQuery, TokenUpdateData, TokenDeleteResult } from '~/types'; +import logger from '~/config/winston'; + +// Factory function that takes mongoose instance and returns the methods +export function createTokenMethods(mongoose: typeof import('mongoose')) { + /** + * Creates a new Token instance. + */ + async function createToken(tokenData: TokenCreateData): Promise { + try { + const Token = mongoose.models.Token; + const currentTime = new Date(); + const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000); + + const newTokenData = { + ...tokenData, + createdAt: currentTime, + expiresAt, + }; + + return await Token.create(newTokenData); + } catch (error) { + logger.debug('An error occurred while creating token:', error); + throw error; + } + } + + /** + * Updates a Token document that matches the provided query. + */ + async function updateToken( + query: TokenQuery, + updateData: TokenUpdateData, + ): Promise { + try { + const Token = mongoose.models.Token; + return await Token.findOneAndUpdate(query, updateData, { new: true }); + } catch (error) { + logger.debug('An error occurred while updating token:', error); + throw error; + } + } + + /** + * Deletes all Token documents that match the provided token, user ID, or email. + */ + async function deleteTokens(query: TokenQuery): Promise { + try { + const Token = mongoose.models.Token; + return await Token.deleteMany({ + $or: [ + { userId: query.userId }, + { token: query.token }, + { email: query.email }, + { identifier: query.identifier }, + ], + }); + } catch (error) { + logger.debug('An error occurred while deleting tokens:', error); + throw error; + } + } + + /** + * Finds a Token document that matches the provided query. + */ + async function findToken(query: TokenQuery): Promise { + try { + const Token = mongoose.models.Token; + const conditions = []; + + if (query.userId) { + conditions.push({ userId: query.userId }); + } + if (query.token) { + conditions.push({ token: query.token }); + } + if (query.email) { + conditions.push({ email: query.email }); + } + if (query.identifier) { + conditions.push({ identifier: query.identifier }); + } + + const token = await Token.findOne({ + $and: conditions, + }).lean(); + + return token as IToken | null; + } catch (error) { + logger.debug('An error occurred while finding token:', error); + throw error; + } + } + + // Return all methods + return { + findToken, + createToken, + updateToken, + deleteTokens, + }; +} + +export type TokenMethods = ReturnType; diff --git a/packages/data-schemas/src/methods/user.ts b/packages/data-schemas/src/methods/user.ts new file mode 100644 index 000000000..5c7b2e40d --- /dev/null +++ b/packages/data-schemas/src/methods/user.ts @@ -0,0 +1,174 @@ +import mongoose, { FilterQuery } from 'mongoose'; +import type { IUser, BalanceConfig, UserCreateData, UserUpdateResult } from '~/types'; +import { signPayload } from '~/crypto'; + +/** Factory function that takes mongoose instance and returns the methods */ +export function createUserMethods(mongoose: typeof import('mongoose')) { + /** + * Search for a single user based on partial data and return matching user document as plain object. + */ + async function findUser( + searchCriteria: FilterQuery, + fieldsToSelect?: string | string[] | null, + ): Promise { + const User = mongoose.models.User; + const query = User.findOne(searchCriteria); + if (fieldsToSelect) { + query.select(fieldsToSelect); + } + return (await query.lean()) as IUser | null; + } + + /** + * Count the number of user documents in the collection based on the provided filter. + */ + async function countUsers(filter: FilterQuery = {}): Promise { + const User = mongoose.models.User; + return await User.countDocuments(filter); + } + + /** + * Creates a new user, optionally with a TTL of 1 week. + */ + async function createUser( + data: UserCreateData, + balanceConfig?: BalanceConfig, + disableTTL: boolean = true, + returnUser: boolean = false, + ): Promise> { + const User = mongoose.models.User; + const Balance = mongoose.models.Balance; + + const userData: Partial = { + ...data, + expiresAt: disableTTL ? undefined : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds + }; + + if (disableTTL) { + delete userData.expiresAt; + } + + const user = await User.create(userData); + + // If balance is enabled, create or update a balance record for the user + if (balanceConfig?.enabled && balanceConfig?.startBalance) { + const update: { + $inc: { tokenCredits: number }; + $set?: { + autoRefillEnabled: boolean; + refillIntervalValue: number; + refillIntervalUnit: string; + refillAmount: number; + }; + } = { + $inc: { tokenCredits: balanceConfig.startBalance }, + }; + + if ( + balanceConfig.autoRefillEnabled && + balanceConfig.refillIntervalValue != null && + balanceConfig.refillIntervalUnit != null && + balanceConfig.refillAmount != null + ) { + update.$set = { + autoRefillEnabled: true, + refillIntervalValue: balanceConfig.refillIntervalValue, + refillIntervalUnit: balanceConfig.refillIntervalUnit, + refillAmount: balanceConfig.refillAmount, + }; + } + + await Balance.findOneAndUpdate({ user: user._id }, update, { + upsert: true, + new: true, + }).lean(); + } + + if (returnUser) { + return user.toObject() as Partial; + } + return user._id as mongoose.Types.ObjectId; + } + + /** + * Update a user with new data without overwriting existing properties. + */ + async function updateUser(userId: string, updateData: Partial): Promise { + const User = mongoose.models.User; + const updateOperation = { + $set: updateData, + $unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL + }; + return (await User.findByIdAndUpdate(userId, updateOperation, { + new: true, + runValidators: true, + }).lean()) as IUser | null; + } + + /** + * Retrieve a user by ID and convert the found user document to a plain object. + */ + async function getUserById( + userId: string, + fieldsToSelect?: string | string[] | null, + ): Promise { + const User = mongoose.models.User; + const query = User.findById(userId); + if (fieldsToSelect) { + query.select(fieldsToSelect); + } + return (await query.lean()) as IUser | null; + } + + /** + * Delete a user by their unique ID. + */ + async function deleteUserById(userId: string): Promise { + try { + const User = mongoose.models.User; + const result = await User.deleteOne({ _id: userId }); + if (result.deletedCount === 0) { + return { deletedCount: 0, message: 'No user found with that ID.' }; + } + return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new Error('Error deleting user: ' + errorMessage); + } + } + + /** + * Generates a JWT token for a given user. + */ + async function generateToken(user: IUser): Promise { + if (!user) { + throw new Error('No user provided'); + } + + const expires = eval(process.env.SESSION_EXPIRY ?? '0') ?? 1000 * 60 * 15; + + return await signPayload({ + payload: { + id: user._id, + username: user.username, + provider: user.provider, + email: user.email, + }, + secret: process.env.JWT_SECRET, + expirationTime: expires / 1000, + }); + } + + // Return all methods + return { + findUser, + countUsers, + createUser, + updateUser, + getUserById, + deleteUserById, + generateToken, + }; +} + +export type UserMethods = ReturnType; diff --git a/packages/data-schemas/src/models/action.ts b/packages/data-schemas/src/models/action.ts new file mode 100644 index 000000000..477822246 --- /dev/null +++ b/packages/data-schemas/src/models/action.ts @@ -0,0 +1,9 @@ +import actionSchema from '~/schema/action'; +import type { IAction } from '~/types'; + +/** + * Creates or returns the Action model using the provided mongoose instance and schema + */ +export function createActionModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Action || mongoose.model('Action', actionSchema); +} diff --git a/packages/data-schemas/src/models/agent.ts b/packages/data-schemas/src/models/agent.ts new file mode 100644 index 000000000..bff6bae60 --- /dev/null +++ b/packages/data-schemas/src/models/agent.ts @@ -0,0 +1,9 @@ +import agentSchema from '~/schema/agent'; +import type { IAgent } from '~/types'; + +/** + * Creates or returns the Agent model using the provided mongoose instance and schema + */ +export function createAgentModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Agent || mongoose.model('Agent', agentSchema); +} diff --git a/packages/data-schemas/src/models/assistant.ts b/packages/data-schemas/src/models/assistant.ts new file mode 100644 index 000000000..bf8a9f5ac --- /dev/null +++ b/packages/data-schemas/src/models/assistant.ts @@ -0,0 +1,9 @@ +import assistantSchema from '~/schema/assistant'; +import type { IAssistant } from '~/types'; + +/** + * Creates or returns the Assistant model using the provided mongoose instance and schema + */ +export function createAssistantModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Assistant || mongoose.model('Assistant', assistantSchema); +} diff --git a/packages/data-schemas/src/models/balance.ts b/packages/data-schemas/src/models/balance.ts new file mode 100644 index 000000000..e7ace3893 --- /dev/null +++ b/packages/data-schemas/src/models/balance.ts @@ -0,0 +1,9 @@ +import balanceSchema from '~/schema/balance'; +import type * as t from '~/types'; + +/** + * Creates or returns the Balance model using the provided mongoose instance and schema + */ +export function createBalanceModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Balance || mongoose.model('Balance', balanceSchema); +} diff --git a/packages/data-schemas/src/models/banner.ts b/packages/data-schemas/src/models/banner.ts new file mode 100644 index 000000000..7be6e2e07 --- /dev/null +++ b/packages/data-schemas/src/models/banner.ts @@ -0,0 +1,9 @@ +import bannerSchema from '~/schema/banner'; +import type { IBanner } from '~/types'; + +/** + * Creates or returns the Banner model using the provided mongoose instance and schema + */ +export function createBannerModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Banner || mongoose.model('Banner', bannerSchema); +} diff --git a/packages/data-schemas/src/models/conversationTag.ts b/packages/data-schemas/src/models/conversationTag.ts new file mode 100644 index 000000000..902915a6c --- /dev/null +++ b/packages/data-schemas/src/models/conversationTag.ts @@ -0,0 +1,11 @@ +import conversationTagSchema, { IConversationTag } from '~/schema/conversationTag'; + +/** + * Creates or returns the ConversationTag model using the provided mongoose instance and schema + */ +export function createConversationTagModel(mongoose: typeof import('mongoose')) { + return ( + mongoose.models.ConversationTag || + mongoose.model('ConversationTag', conversationTagSchema) + ); +} diff --git a/packages/data-schemas/src/models/convo.ts b/packages/data-schemas/src/models/convo.ts new file mode 100644 index 000000000..13a36e206 --- /dev/null +++ b/packages/data-schemas/src/models/convo.ts @@ -0,0 +1,11 @@ +import type * as t from '~/types'; +import convoSchema from '~/schema/convo'; + +/** + * Creates or returns the Conversation model using the provided mongoose instance and schema + */ +export function createConversationModel(mongoose: typeof import('mongoose')) { + return ( + mongoose.models.Conversation || mongoose.model('Conversation', convoSchema) + ); +} diff --git a/packages/data-schemas/src/models/file.ts b/packages/data-schemas/src/models/file.ts new file mode 100644 index 000000000..c12dbec14 --- /dev/null +++ b/packages/data-schemas/src/models/file.ts @@ -0,0 +1,9 @@ +import fileSchema from '~/schema/file'; +import type { IMongoFile } from '~/types'; + +/** + * Creates or returns the File model using the provided mongoose instance and schema + */ +export function createFileModel(mongoose: typeof import('mongoose')) { + return mongoose.models.File || mongoose.model('File', fileSchema); +} diff --git a/packages/data-schemas/src/models/index.ts b/packages/data-schemas/src/models/index.ts new file mode 100644 index 000000000..5be6a67d0 --- /dev/null +++ b/packages/data-schemas/src/models/index.ts @@ -0,0 +1,52 @@ +import { createUserModel } from './user'; +import { createTokenModel } from './token'; +import { createSessionModel } from './session'; +import { createBalanceModel } from './balance'; +import { createConversationModel } from './convo'; +import { createMessageModel } from './message'; +import { createAgentModel } from './agent'; +import { createRoleModel } from './role'; +import { createActionModel } from './action'; +import { createAssistantModel } from './assistant'; +import { createFileModel } from './file'; +import { createBannerModel } from './banner'; +import { createProjectModel } from './project'; +import { createKeyModel } from './key'; +import { createPluginAuthModel } from './pluginAuth'; +import { createTransactionModel } from './transaction'; +import { createPresetModel } from './preset'; +import { createPromptModel } from './prompt'; +import { createPromptGroupModel } from './promptGroup'; +import { createConversationTagModel } from './conversationTag'; +import { createSharedLinkModel } from './sharedLink'; +import { createToolCallModel } from './toolCall'; + +/** + * Creates all database models for all collections + */ +export function createModels(mongoose: typeof import('mongoose')) { + return { + User: createUserModel(mongoose), + Token: createTokenModel(mongoose), + Session: createSessionModel(mongoose), + Balance: createBalanceModel(mongoose), + Conversation: createConversationModel(mongoose), + Message: createMessageModel(mongoose), + Agent: createAgentModel(mongoose), + Role: createRoleModel(mongoose), + Action: createActionModel(mongoose), + Assistant: createAssistantModel(mongoose), + File: createFileModel(mongoose), + Banner: createBannerModel(mongoose), + Project: createProjectModel(mongoose), + Key: createKeyModel(mongoose), + PluginAuth: createPluginAuthModel(mongoose), + Transaction: createTransactionModel(mongoose), + Preset: createPresetModel(mongoose), + Prompt: createPromptModel(mongoose), + PromptGroup: createPromptGroupModel(mongoose), + ConversationTag: createConversationTagModel(mongoose), + SharedLink: createSharedLinkModel(mongoose), + ToolCall: createToolCallModel(mongoose), + }; +} diff --git a/packages/data-schemas/src/models/key.ts b/packages/data-schemas/src/models/key.ts new file mode 100644 index 000000000..6e2ff70c9 --- /dev/null +++ b/packages/data-schemas/src/models/key.ts @@ -0,0 +1,8 @@ +import keySchema, { IKey } from '~/schema/key'; + +/** + * Creates or returns the Key model using the provided mongoose instance and schema + */ +export function createKeyModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Key || mongoose.model('Key', keySchema); +} diff --git a/packages/data-schemas/src/models/message.ts b/packages/data-schemas/src/models/message.ts new file mode 100644 index 000000000..cb5bd9e7d --- /dev/null +++ b/packages/data-schemas/src/models/message.ts @@ -0,0 +1,9 @@ +import messageSchema from '~/schema/message'; +import type * as t from '~/types'; + +/** + * Creates or returns the Message model using the provided mongoose instance and schema + */ +export function createMessageModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Message || mongoose.model('Message', messageSchema); +} diff --git a/packages/data-schemas/src/models/pluginAuth.ts b/packages/data-schemas/src/models/pluginAuth.ts new file mode 100644 index 000000000..cf466a145 --- /dev/null +++ b/packages/data-schemas/src/models/pluginAuth.ts @@ -0,0 +1,8 @@ +import pluginAuthSchema, { IPluginAuth } from '~/schema/pluginAuth'; + +/** + * Creates or returns the PluginAuth model using the provided mongoose instance and schema + */ +export function createPluginAuthModel(mongoose: typeof import('mongoose')) { + return mongoose.models.PluginAuth || mongoose.model('PluginAuth', pluginAuthSchema); +} diff --git a/api/models/plugins/mongoMeili.js b/packages/data-schemas/src/models/plugins/mongoMeili.ts similarity index 51% rename from api/models/plugins/mongoMeili.js rename to packages/data-schemas/src/models/plugins/mongoMeili.ts index 75e3738e5..111f9e0bb 100644 --- a/api/models/plugins/mongoMeili.js +++ b/packages/data-schemas/src/models/plugins/mongoMeili.ts @@ -1,34 +1,89 @@ -const _ = require('lodash'); -const mongoose = require('mongoose'); -const { MeiliSearch } = require('meilisearch'); -const { parseTextParts, ContentTypes } = require('librechat-data-provider'); -const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc'); -const logger = require('~/config/meiliLogger'); +import _ from 'lodash'; +import { MeiliSearch, Index } from 'meilisearch'; +import mongoose, { Schema, Document, Model, Query } from 'mongoose'; +import logger from '~/config/meiliLogger'; + +interface MongoMeiliOptions { + host: string; + apiKey: string; + indexName: string; + primaryKey: string; +} + +interface MeiliIndexable { + [key: string]: unknown; + _meiliIndex?: boolean; +} + +interface ContentItem { + type: string; + text?: string; +} + +interface DocumentWithMeiliIndex extends Document { + _meiliIndex?: boolean; + preprocessObjectForIndex?: () => Record; + addObjectToMeili?: () => Promise; + updateObjectToMeili?: () => Promise; + deleteObjectFromMeili?: () => Promise; + postSaveHook?: () => void; + postUpdateHook?: () => void; + postRemoveHook?: () => void; + conversationId?: string; + content?: ContentItem[]; + messageId?: string; + unfinished?: boolean; + messages?: unknown[]; + title?: string; + toJSON(): Record; +} + +interface SchemaWithMeiliMethods extends Model { + syncWithMeili(): Promise; + setMeiliIndexSettings(settings: Record): Promise; + meiliSearch(q: string, params: Record, populate: boolean): Promise; +} // Environment flags /** * Flag to indicate if search is enabled based on environment variables. - * @type {boolean} */ -const searchEnabled = process.env.SEARCH && process.env.SEARCH.toLowerCase() === 'true'; +const searchEnabled = process.env.SEARCH != null && process.env.SEARCH.toLowerCase() === 'true'; /** * Flag to indicate if MeiliSearch is enabled based on required environment variables. - * @type {boolean} */ -const meiliEnabled = process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY && searchEnabled; +const meiliEnabled = + process.env.MEILI_HOST != null && process.env.MEILI_MASTER_KEY != null && searchEnabled; + +/** + * Local implementation of parseTextParts to avoid dependency on librechat-data-provider + * Extracts text content from an array of content items + */ +const parseTextParts = (content: ContentItem[]): string => { + if (!Array.isArray(content)) { + return ''; + } + + return content + .filter((item) => item.type === 'text' && typeof item.text === 'string') + .map((item) => item.text) + .join(' ') + .trim(); +}; + +/** + * Local implementation to handle Bing convoId conversion + */ +const cleanUpPrimaryKeyValue = (value: string): string => { + return value.replace(/--/g, '|'); +}; /** * Validates the required options for configuring the mongoMeili plugin. - * - * @param {Object} options - The configuration options. - * @param {string} options.host - The MeiliSearch host. - * @param {string} options.apiKey - The MeiliSearch API key. - * @param {string} options.indexName - The name of the index. - * @throws {Error} Throws an error if any required option is missing. */ -const validateOptions = function (options) { - const requiredKeys = ['host', 'apiKey', 'indexName']; +const validateOptions = (options: Partial): void => { + const requiredKeys: (keyof MongoMeiliOptions)[] = ['host', 'apiKey', 'indexName']; requiredKeys.forEach((key) => { if (!options[key]) { throw new Error(`Missing mongoMeili Option: ${key}`); @@ -41,13 +96,18 @@ const validateOptions = function (options) { * This class contains static and instance methods to synchronize and manage the MeiliSearch index * corresponding to the MongoDB collection. * - * @param {Object} config - Configuration object. - * @param {Object} config.index - The MeiliSearch index object. - * @param {Array} config.attributesToIndex - List of attributes to index. - * @returns {Function} A class definition that will be loaded into the Mongoose schema. + * @param config - Configuration object. + * @param config.index - The MeiliSearch index object. + * @param config.attributesToIndex - List of attributes to index. + * @returns A class definition that will be loaded into the Mongoose schema. */ -const createMeiliMongooseModel = function ({ index, attributesToIndex }) { - // The primary key is assumed to be the first attribute in the attributesToIndex array. +const createMeiliMongooseModel = ({ + index, + attributesToIndex, +}: { + index: Index; + attributesToIndex: string[]; +}) => { const primaryKey = attributesToIndex[0]; class MeiliMongooseModel { @@ -68,24 +128,24 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { * * @returns {Promise} Resolves when the synchronization is complete. */ - static async syncWithMeili() { + static async syncWithMeili(this: SchemaWithMeiliMethods): Promise { try { let moreDocuments = true; - // Retrieve all MongoDB documents from the collection as plain JavaScript objects. const mongoDocuments = await this.find().lean(); - // Helper function to format a document by selecting only the attributes to index - // and omitting keys starting with '$'. - const format = (doc) => + const format = (doc: Record) => _.omitBy(_.pick(doc, attributesToIndex), (v, k) => k.startsWith('$')); - // Build a map of MongoDB documents for quick lookup based on the primary key. - const mongoMap = new Map(mongoDocuments.map((doc) => [doc[primaryKey], format(doc)])); - const indexMap = new Map(); + const mongoMap = new Map( + mongoDocuments.map((doc) => { + const typedDoc = doc as Record; + return [typedDoc[primaryKey], format(typedDoc)]; + }), + ); + const indexMap = new Map>(); let offset = 0; const batchSize = 1000; - // Fetch documents from the MeiliSearch index in batches. while (moreDocuments) { const batch = await index.getDocuments({ limit: batchSize, offset }); if (batch.results.length === 0) { @@ -99,17 +159,22 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { logger.debug('[syncWithMeili]', { indexMap: indexMap.size, mongoMap: mongoMap.size }); - const updateOps = []; + const updateOps: Array<{ + updateOne: { + filter: Record; + update: { $set: { _meiliIndex: boolean } }; + }; + }> = []; - // Process documents present in the MeiliSearch index. + // Process documents present in the MeiliSearch index for (const [id, doc] of indexMap) { - const update = {}; + const update: Record = {}; update[primaryKey] = id; if (mongoMap.has(id)) { - // If document exists in MongoDB, check for discrepancies in key fields. + const mongoDoc = mongoMap.get(id); if ( - (doc.text && doc.text !== mongoMap.get(id).text) || - (doc.title && doc.title !== mongoMap.get(id).title) + (doc.text && doc.text !== mongoDoc?.text) || + (doc.title && doc.title !== mongoDoc?.title) ) { logger.debug( `[syncWithMeili] ${id} had document discrepancy in ${ @@ -122,33 +187,29 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { await index.addDocuments([doc]); } } else { - // If the document does not exist in MongoDB, delete it from MeiliSearch. - await index.deleteDocument(id); + await index.deleteDocument(id as string); updateOps.push({ updateOne: { filter: update, update: { $set: { _meiliIndex: false } } }, }); } } - // Process documents present in MongoDB. + // Process documents present in MongoDB for (const [id, doc] of mongoMap) { - const update = {}; + const update: Record = {}; update[primaryKey] = id; - // If the document is missing in the Meili index, add it. if (!indexMap.has(id)) { await index.addDocuments([doc]); updateOps.push({ updateOne: { filter: update, update: { $set: { _meiliIndex: true } } }, }); } else if (doc._meiliIndex === false) { - // If the document exists but is marked as not indexed, update the flag. updateOps.push({ updateOne: { filter: update, update: { $set: { _meiliIndex: true } } }, }); } } - // Execute bulk update operations in MongoDB to update the _meiliIndex flags. if (updateOps.length > 0) { await this.collection.bulkWrite(updateOps); logger.debug( @@ -163,33 +224,30 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { } /** - * Updates settings for the MeiliSearch index. - * - * @param {Object} settings - The settings to update on the MeiliSearch index. - * @returns {Promise} Promise resolving to the update result. + * Updates settings for the MeiliSearch index */ - static async setMeiliIndexSettings(settings) { + static async setMeiliIndexSettings(settings: Record): Promise { return await index.updateSettings(settings); } /** - * Searches the MeiliSearch index and optionally populates the results with data from MongoDB. - * - * @param {string} q - The search query. - * @param {Object} params - Additional search parameters for MeiliSearch. - * @param {boolean} populate - Whether to populate search hits with full MongoDB documents. - * @returns {Promise} The search results with populated hits if requested. + * Searches the MeiliSearch index and optionally populates results */ - static async meiliSearch(q, params, populate) { + static async meiliSearch( + this: SchemaWithMeiliMethods, + q: string, + params: Record, + populate: boolean, + ): Promise { const data = await index.search(q, params); if (populate) { - // Build a query using the primary key values from the search hits. - const query = {}; - query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey])); + const query: Record = {}; + query[primaryKey] = _.map(data.hits, (hit) => + cleanUpPrimaryKeyValue(hit[primaryKey] as string), + ); - // Build a projection object, including only keys that do not start with '$'. - const projection = Object.keys(this.schema.obj).reduce( + const projection = Object.keys(this.schema.obj).reduce>( (results, key) => { if (!key.startsWith('$')) { results[key] = 1; @@ -199,17 +257,18 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { { _id: 1, __v: 1 }, ); - // Retrieve the full documents from MongoDB. const hitsFromMongoose = await this.find(query, projection).lean(); - // Merge the MongoDB documents with the search hits. - const populatedHits = data.hits.map(function (hit) { - const query = {}; - query[primaryKey] = hit[primaryKey]; - const originalHit = _.find(hitsFromMongoose, query); + const populatedHits = data.hits.map((hit) => { + const queryObj: Record = {}; + queryObj[primaryKey] = hit[primaryKey]; + const originalHit = _.find(hitsFromMongoose, (item) => { + const typedItem = item as Record; + return typedItem[primaryKey] === hit[primaryKey]; + }); return { - ...(originalHit ?? {}), + ...(originalHit && typeof originalHit === 'object' ? originalHit : {}), ...hit, }; }); @@ -220,21 +279,18 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { } /** - * Preprocesses the current document for indexing. - * - * This method: - * - Picks only the defined attributes to index. - * - Omits any keys starting with '$'. - * - Replaces pipe characters ('|') in `conversationId` with '--'. - * - Extracts and concatenates text from an array of content items. - * - * @returns {Object} The preprocessed object ready for indexing. + * Preprocesses the current document for indexing */ - preprocessObjectForIndex() { + preprocessObjectForIndex(this: DocumentWithMeiliIndex): Record { const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) => k.startsWith('$'), ); - if (object.conversationId && object.conversationId.includes('|')) { + + if ( + object.conversationId && + typeof object.conversationId === 'string' && + object.conversationId.includes('|') + ) { object.conversationId = object.conversationId.replace(/\|/g, '--'); } @@ -247,31 +303,26 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { } /** - * Adds the current document to the MeiliSearch index. - * - * The method preprocesses the document, adds it to MeiliSearch, and then updates - * the MongoDB document's `_meiliIndex` flag to true. - * - * @returns {Promise} + * Adds the current document to the MeiliSearch index */ - async addObjectToMeili() { - const object = this.preprocessObjectForIndex(); + async addObjectToMeili(this: DocumentWithMeiliIndex): Promise { + const object = this.preprocessObjectForIndex!(); try { await index.addDocuments([object]); } catch (error) { - // Error handling can be enhanced as needed. logger.error('[addObjectToMeili] Error adding document to Meili', error); } - await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } }); + await this.collection.updateMany( + { _id: this._id as mongoose.Types.ObjectId }, + { $set: { _meiliIndex: true } }, + ); } /** - * Updates the current document in the MeiliSearch index. - * - * @returns {Promise} + * Updates the current document in the MeiliSearch index */ - async updateObjectToMeili() { + async updateObjectToMeili(this: DocumentWithMeiliIndex): Promise { const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) => k.startsWith('$'), ); @@ -283,8 +334,8 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { * * @returns {Promise} */ - async deleteObjectFromMeili() { - await index.deleteDocument(this._id); + async deleteObjectFromMeili(this: DocumentWithMeiliIndex): Promise { + await index.deleteDocument(this._id as string); } /** @@ -293,11 +344,11 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { * If the document is already indexed (i.e. `_meiliIndex` is true), it updates it; * otherwise, it adds the document to the index. */ - postSaveHook() { + postSaveHook(this: DocumentWithMeiliIndex): void { if (this._meiliIndex) { - this.updateObjectToMeili(); + this.updateObjectToMeili!(); } else { - this.addObjectToMeili(); + this.addObjectToMeili!(); } } @@ -307,9 +358,9 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { * This hook is triggered after a document update, ensuring that changes are * propagated to the MeiliSearch index if the document is indexed. */ - postUpdateHook() { + postUpdateHook(this: DocumentWithMeiliIndex): void { if (this._meiliIndex) { - this.updateObjectToMeili(); + this.updateObjectToMeili!(); } } @@ -319,9 +370,9 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { * This hook is triggered after a document is removed, ensuring that the document * is also removed from the MeiliSearch index if it was previously indexed. */ - postRemoveHook() { + postRemoveHook(this: DocumentWithMeiliIndex): void { if (this._meiliIndex) { - this.deleteObjectFromMeili(); + this.deleteObjectFromMeili!(); } } } @@ -339,14 +390,14 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) { * - Loads class methods for syncing, searching, and managing documents in MeiliSearch. * - Registers Mongoose hooks (post-save, post-update, post-remove, etc.) to maintain index consistency. * - * @param {mongoose.Schema} schema - The Mongoose schema to which the plugin is applied. - * @param {Object} options - Configuration options. - * @param {string} options.host - The MeiliSearch host. - * @param {string} options.apiKey - The MeiliSearch API key. - * @param {string} options.indexName - The name of the MeiliSearch index. - * @param {string} options.primaryKey - The primary key field for indexing. + * @param schema - The Mongoose schema to which the plugin is applied. + * @param options - Configuration options. + * @param options.host - The MeiliSearch host. + * @param options.apiKey - The MeiliSearch API key. + * @param options.indexName - The name of the MeiliSearch index. + * @param options.primaryKey - The primary key field for indexing. */ -module.exports = function mongoMeili(schema, options) { +export default function mongoMeili(schema: Schema, options: MongoMeiliOptions): void { validateOptions(options); // Add _meiliIndex field to the schema to track if a document has been indexed in MeiliSearch. @@ -361,44 +412,31 @@ module.exports = function mongoMeili(schema, options) { const { host, apiKey, indexName, primaryKey } = options; - // Setup the MeiliSearch client. const client = new MeiliSearch({ host, apiKey }); - - // Create the index asynchronously if it doesn't exist. client.createIndex(indexName, { primaryKey }); + const index = client.index(indexName); - // Setup the MeiliSearch index for this schema. - const index = client.index(indexName); - - // Collect attributes from the schema that should be indexed. - const attributesToIndex = [ - ..._.reduce( - schema.obj, - function (results, value, key) { - return value.meiliIndex ? [...results, key] : results; - }, - [], - ), + // Collect attributes from the schema that should be indexed + const attributesToIndex: string[] = [ + ...Object.entries(schema.obj).reduce((results, [key, value]) => { + const schemaValue = value as { meiliIndex?: boolean }; + return schemaValue.meiliIndex ? [...results, key] : results; + }, []), ]; - // Load the class methods into the schema. - schema.loadClass(createMeiliMongooseModel({ index, indexName, client, attributesToIndex })); + schema.loadClass(createMeiliMongooseModel({ index, attributesToIndex })); - // Register Mongoose hooks to synchronize with MeiliSearch. - - // Post-save: synchronize after a document is saved. - schema.post('save', function (doc) { - doc.postSaveHook(); + // Register Mongoose hooks + schema.post('save', function (doc: DocumentWithMeiliIndex) { + doc.postSaveHook?.(); }); - // Post-update: synchronize after a document is updated. - schema.post('update', function (doc) { - doc.postUpdateHook(); + schema.post('updateOne', function (doc: DocumentWithMeiliIndex) { + doc.postUpdateHook?.(); }); - // Post-remove: synchronize after a document is removed. - schema.post('remove', function (doc) { - doc.postRemoveHook(); + schema.post('deleteOne', function (doc: DocumentWithMeiliIndex) { + doc.postRemoveHook?.(); }); // Pre-deleteMany hook: remove corresponding documents from MeiliSearch when multiple documents are deleted. @@ -408,22 +446,28 @@ module.exports = function mongoMeili(schema, options) { } try { - // Check if the schema has a "messages" field to determine if it's a conversation schema. + const conditions = (this as Query).getQuery(); + if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) { const convoIndex = client.index('convos'); - const deletedConvos = await mongoose.model('Conversation').find(this._conditions).lean(); - const promises = deletedConvos.map((convo) => - convoIndex.deleteDocument(convo.conversationId), + const deletedConvos = await mongoose + .model('Conversation') + .find(conditions as mongoose.FilterQuery) + .lean(); + const promises = deletedConvos.map((convo: Record) => + convoIndex.deleteDocument(convo.conversationId as string), ); await Promise.all(promises); } - // Check if the schema has a "messageId" field to determine if it's a message schema. if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) { const messageIndex = client.index('messages'); - const deletedMessages = await mongoose.model('Message').find(this._conditions).lean(); - const promises = deletedMessages.map((message) => - messageIndex.deleteDocument(message.messageId), + const deletedMessages = await mongoose + .model('Message') + .find(conditions as mongoose.FilterQuery) + .lean(); + const promises = deletedMessages.map((message: Record) => + messageIndex.deleteDocument(message.messageId as string), ); await Promise.all(promises); } @@ -439,37 +483,33 @@ module.exports = function mongoMeili(schema, options) { } }); - // Post-findOneAndUpdate hook: update MeiliSearch index after a document is updated via findOneAndUpdate. - schema.post('findOneAndUpdate', async function (doc) { + // Post-findOneAndUpdate hook + schema.post('findOneAndUpdate', async function (doc: DocumentWithMeiliIndex) { if (!meiliEnabled) { return; } - // If the document is unfinished, do not update the index. if (doc.unfinished) { return; } - let meiliDoc; - // For conversation documents, try to fetch the document from the "convos" index. + let meiliDoc: Record | undefined; if (doc.messages) { try { - meiliDoc = await client.index('convos').getDocument(doc.conversationId); - } catch (error) { + meiliDoc = await client.index('convos').getDocument(doc.conversationId as string); + } catch (error: unknown) { logger.debug( '[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' + doc.conversationId, - error, + error as Record, ); } } - // If the MeiliSearch document exists and the title is unchanged, do nothing. if (meiliDoc && meiliDoc.title === doc.title) { return; } - // Otherwise, trigger a post-save hook to synchronize the document. - doc.postSaveHook(); + doc.postSaveHook?.(); }); -}; +} diff --git a/packages/data-schemas/src/models/preset.ts b/packages/data-schemas/src/models/preset.ts new file mode 100644 index 000000000..c5b156e55 --- /dev/null +++ b/packages/data-schemas/src/models/preset.ts @@ -0,0 +1,8 @@ +import presetSchema, { IPreset } from '~/schema/preset'; + +/** + * Creates or returns the Preset model using the provided mongoose instance and schema + */ +export function createPresetModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Preset || mongoose.model('Preset', presetSchema); +} diff --git a/packages/data-schemas/src/models/project.ts b/packages/data-schemas/src/models/project.ts new file mode 100644 index 000000000..c68f532bc --- /dev/null +++ b/packages/data-schemas/src/models/project.ts @@ -0,0 +1,8 @@ +import projectSchema, { IMongoProject } from '~/schema/project'; + +/** + * Creates or returns the Project model using the provided mongoose instance and schema + */ +export function createProjectModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Project || mongoose.model('Project', projectSchema); +} diff --git a/packages/data-schemas/src/models/prompt.ts b/packages/data-schemas/src/models/prompt.ts new file mode 100644 index 000000000..74cc4ea2d --- /dev/null +++ b/packages/data-schemas/src/models/prompt.ts @@ -0,0 +1,8 @@ +import promptSchema, { IPrompt } from '~/schema/prompt'; + +/** + * Creates or returns the Prompt model using the provided mongoose instance and schema + */ +export function createPromptModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Prompt || mongoose.model('Prompt', promptSchema); +} diff --git a/packages/data-schemas/src/models/promptGroup.ts b/packages/data-schemas/src/models/promptGroup.ts new file mode 100644 index 000000000..41e3d2e34 --- /dev/null +++ b/packages/data-schemas/src/models/promptGroup.ts @@ -0,0 +1,11 @@ +import promptGroupSchema, { IPromptGroupDocument } from '~/schema/promptGroup'; + +/** + * Creates or returns the PromptGroup model using the provided mongoose instance and schema + */ +export function createPromptGroupModel(mongoose: typeof import('mongoose')) { + return ( + mongoose.models.PromptGroup || + mongoose.model('PromptGroup', promptGroupSchema) + ); +} diff --git a/packages/data-schemas/src/models/role.ts b/packages/data-schemas/src/models/role.ts new file mode 100644 index 000000000..ccc56af1d --- /dev/null +++ b/packages/data-schemas/src/models/role.ts @@ -0,0 +1,9 @@ +import roleSchema from '~/schema/role'; +import type { IRole } from '~/types'; + +/** + * Creates or returns the Role model using the provided mongoose instance and schema + */ +export function createRoleModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Role || mongoose.model('Role', roleSchema); +} diff --git a/packages/data-schemas/src/models/session.ts b/packages/data-schemas/src/models/session.ts new file mode 100644 index 000000000..3d4eba276 --- /dev/null +++ b/packages/data-schemas/src/models/session.ts @@ -0,0 +1,9 @@ +import sessionSchema from '~/schema/session'; +import type * as t from '~/types'; + +/** + * Creates or returns the Session model using the provided mongoose instance and schema + */ +export function createSessionModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Session || mongoose.model('Session', sessionSchema); +} diff --git a/packages/data-schemas/src/models/sharedLink.ts b/packages/data-schemas/src/models/sharedLink.ts new file mode 100644 index 000000000..662f9aafc --- /dev/null +++ b/packages/data-schemas/src/models/sharedLink.ts @@ -0,0 +1,8 @@ +import shareSchema, { ISharedLink } from '~/schema/share'; + +/** + * Creates or returns the SharedLink model using the provided mongoose instance and schema + */ +export function createSharedLinkModel(mongoose: typeof import('mongoose')) { + return mongoose.models.SharedLink || mongoose.model('SharedLink', shareSchema); +} diff --git a/packages/data-schemas/src/models/token.ts b/packages/data-schemas/src/models/token.ts new file mode 100644 index 000000000..870233f61 --- /dev/null +++ b/packages/data-schemas/src/models/token.ts @@ -0,0 +1,9 @@ +import tokenSchema from '~/schema/token'; +import type * as t from '~/types'; + +/** + * Creates or returns the Token model using the provided mongoose instance and schema + */ +export function createTokenModel(mongoose: typeof import('mongoose')) { + return mongoose.models.Token || mongoose.model('Token', tokenSchema); +} diff --git a/packages/data-schemas/src/models/toolCall.ts b/packages/data-schemas/src/models/toolCall.ts new file mode 100644 index 000000000..18292fd8e --- /dev/null +++ b/packages/data-schemas/src/models/toolCall.ts @@ -0,0 +1,8 @@ +import toolCallSchema, { IToolCallData } from '~/schema/toolCall'; + +/** + * Creates or returns the ToolCall model using the provided mongoose instance and schema + */ +export function createToolCallModel(mongoose: typeof import('mongoose')) { + return mongoose.models.ToolCall || mongoose.model('ToolCall', toolCallSchema); +} diff --git a/packages/data-schemas/src/models/transaction.ts b/packages/data-schemas/src/models/transaction.ts new file mode 100644 index 000000000..52a33b86a --- /dev/null +++ b/packages/data-schemas/src/models/transaction.ts @@ -0,0 +1,10 @@ +import transactionSchema, { ITransaction } from '~/schema/transaction'; + +/** + * Creates or returns the Transaction model using the provided mongoose instance and schema + */ +export function createTransactionModel(mongoose: typeof import('mongoose')) { + return ( + mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema) + ); +} diff --git a/packages/data-schemas/src/models/user.ts b/packages/data-schemas/src/models/user.ts new file mode 100644 index 000000000..1aef66f6d --- /dev/null +++ b/packages/data-schemas/src/models/user.ts @@ -0,0 +1,9 @@ +import userSchema from '~/schema/user'; +import type * as t from '~/types'; + +/** + * Creates or returns the User model using the provided mongoose instance and schema + */ +export function createUserModel(mongoose: typeof import('mongoose')) { + return mongoose.models.User || mongoose.model('User', userSchema); +} diff --git a/packages/data-schemas/src/schema/action.ts b/packages/data-schemas/src/schema/action.ts index b47ea5f0f..4d5f64a0e 100644 --- a/packages/data-schemas/src/schema/action.ts +++ b/packages/data-schemas/src/schema/action.ts @@ -1,31 +1,5 @@ -import mongoose, { Schema, Document } from 'mongoose'; - -export interface IAction extends Document { - user: mongoose.Types.ObjectId; - action_id: string; - type: string; - settings?: unknown; - agent_id?: string; - assistant_id?: string; - metadata: { - api_key?: string; - auth: { - authorization_type?: string; - custom_auth_header?: string; - type: 'service_http' | 'oauth' | 'none'; - authorization_content_type?: string; - authorization_url?: string; - client_url?: string; - scope?: string; - token_exchange_method: 'default_post' | 'basic_auth_header' | null; - }; - domain: string; - privacy_policy_url?: string; - raw_spec?: string; - oauth_client_id?: string; - oauth_client_secret?: string; - }; -} +import mongoose, { Schema } from 'mongoose'; +import type { IAction } from '~/types'; // Define the Auth sub-schema with type-safety. const AuthSchema = new Schema( diff --git a/packages/data-schemas/src/schema/agent.ts b/packages/data-schemas/src/schema/agent.ts index 784c4f6c3..733645865 100644 --- a/packages/data-schemas/src/schema/agent.ts +++ b/packages/data-schemas/src/schema/agent.ts @@ -1,33 +1,5 @@ -import { Schema, Document, Types } from 'mongoose'; -export interface IAgent extends Omit { - id: string; - name?: string; - description?: string; - instructions?: string; - avatar?: { - filepath: string; - source: string; - }; - provider: string; - model: string; - model_parameters?: Record; - artifacts?: string; - access_level?: number; - recursion_limit?: number; - tools?: string[]; - tool_kwargs?: Array; - actions?: string[]; - author: Types.ObjectId; - authorName?: string; - hide_sequential_outputs?: boolean; - end_after_tools?: boolean; - agent_ids?: string[]; - isCollaborative?: boolean; - conversation_starters?: string[]; - tool_resources?: unknown; - projectIds?: Types.ObjectId[]; - versions?: Omit[]; -} +import { Schema } from 'mongoose'; +import type { IAgent } from '~/types'; const agentSchema = new Schema( { diff --git a/packages/data-schemas/src/schema/assistant.ts b/packages/data-schemas/src/schema/assistant.ts index e58dc81b2..4f0226d38 100644 --- a/packages/data-schemas/src/schema/assistant.ts +++ b/packages/data-schemas/src/schema/assistant.ts @@ -1,18 +1,5 @@ -import { Schema, Document, Types } from 'mongoose'; - -export interface IAssistant extends Document { - user: Types.ObjectId; - assistant_id: string; - avatar?: { - filepath: string; - source: string; - }; - conversation_starters?: string[]; - access_level?: number; - file_ids?: string[]; - actions?: string[]; - append_current_datetime?: boolean; -} +import { Schema } from 'mongoose'; +import type { IAssistant } from '~/types'; const assistantSchema = new Schema( { diff --git a/packages/data-schemas/src/schema/balance.ts b/packages/data-schemas/src/schema/balance.ts index c02871dff..8e786ae38 100644 --- a/packages/data-schemas/src/schema/balance.ts +++ b/packages/data-schemas/src/schema/balance.ts @@ -1,17 +1,7 @@ -import { Schema, Document, Types } from 'mongoose'; +import { Schema } from 'mongoose'; +import type * as t from '~/types'; -export interface IBalance extends Document { - user: Types.ObjectId; - tokenCredits: number; - // Automatic refill settings - autoRefillEnabled: boolean; - refillIntervalValue: number; - refillIntervalUnit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months'; - lastRefill: Date; - refillAmount: number; -} - -const balanceSchema = new Schema({ +const balanceSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: 'User', diff --git a/packages/data-schemas/src/schema/convo.ts b/packages/data-schemas/src/schema/convo.ts index 44a7a6aff..8b0eb5b5c 100644 --- a/packages/data-schemas/src/schema/convo.ts +++ b/packages/data-schemas/src/schema/convo.ts @@ -1,57 +1,7 @@ -import mongoose, { Schema, Document, Types } from 'mongoose'; +import { Schema } from 'mongoose'; +import mongoMeili from '~/models/plugins/mongoMeili'; import { conversationPreset } from './defaults'; - -// @ts-ignore -export interface IConversation extends Document { - conversationId: string; - title?: string; - user?: string; - messages?: Types.ObjectId[]; - agentOptions?: unknown; - // Fields provided by conversationPreset (adjust types as needed) - endpoint?: string; - endpointType?: string; - model?: string; - region?: string; - chatGptLabel?: string; - examples?: unknown[]; - modelLabel?: string; - promptPrefix?: string; - temperature?: number; - top_p?: number; - topP?: number; - topK?: number; - maxOutputTokens?: number; - maxTokens?: number; - presence_penalty?: number; - frequency_penalty?: number; - file_ids?: string[]; - resendImages?: boolean; - promptCache?: boolean; - thinking?: boolean; - thinkingBudget?: number; - system?: string; - resendFiles?: boolean; - imageDetail?: string; - agent_id?: string; - assistant_id?: string; - instructions?: string; - stop?: string[]; - isArchived?: boolean; - iconURL?: string; - greeting?: string; - spec?: string; - tags?: string[]; - tools?: string[]; - maxContextTokens?: number; - max_tokens?: number; - reasoning_effort?: string; - // Additional fields - files?: string[]; - expiredAt?: Date; - createdAt?: Date; - updatedAt?: Date; -} +import { IConversation } from '~/types'; const convoSchema: Schema = new Schema( { @@ -71,9 +21,9 @@ const convoSchema: Schema = new Schema( type: String, index: true, }, - messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }], + messages: [{ type: Schema.Types.ObjectId, ref: 'Message' }], agentOptions: { - type: mongoose.Schema.Types.Mixed, + type: Schema.Types.Mixed, }, ...conversationPreset, agent_id: { @@ -98,4 +48,14 @@ convoSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); convoSchema.index({ createdAt: 1, updatedAt: 1 }); convoSchema.index({ conversationId: 1, user: 1 }, { unique: true }); +if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { + convoSchema.plugin(mongoMeili, { + host: process.env.MEILI_HOST, + apiKey: process.env.MEILI_MASTER_KEY, + /** Note: Will get created automatically if it doesn't exist already */ + indexName: 'convos', + primaryKey: 'conversationId', + }); +} + export default convoSchema; diff --git a/packages/data-schemas/src/schema/file.ts b/packages/data-schemas/src/schema/file.ts index 6d3b22a5a..5bf4d95d8 100644 --- a/packages/data-schemas/src/schema/file.ts +++ b/packages/data-schemas/src/schema/file.ts @@ -1,32 +1,6 @@ -import mongoose, { Schema, Document, Types } from 'mongoose'; +import mongoose, { Schema } from 'mongoose'; import { FileSources } from 'librechat-data-provider'; - -// @ts-ignore -export interface IMongoFile extends Document { - user: Types.ObjectId; - conversationId?: string; - file_id: string; - temp_file_id?: string; - bytes: number; - text?: string; - filename: string; - filepath: string; - object: 'file'; - embedded?: boolean; - type: string; - context?: string; - usage: number; - source: string; - model?: string; - width?: number; - height?: number; - metadata?: { - fileIdentifier?: string; - }; - expiresAt?: Date; - createdAt?: Date; - updatedAt?: Date; -} +import type { IMongoFile } from '~/types'; const file: Schema = new Schema( { diff --git a/packages/data-schemas/src/schema/index.ts b/packages/data-schemas/src/schema/index.ts new file mode 100644 index 000000000..8a69a0cb2 --- /dev/null +++ b/packages/data-schemas/src/schema/index.ts @@ -0,0 +1,23 @@ +export { default as actionSchema } from './action'; +export { default as agentSchema } from './agent'; +export { default as assistantSchema } from './assistant'; +export { default as balanceSchema } from './balance'; +export { default as bannerSchema } from './banner'; +export { default as categoriesSchema } from './categories'; +export { default as conversationTagSchema } from './conversationTag'; +export { default as convoSchema } from './convo'; +export { default as fileSchema } from './file'; +export { default as keySchema } from './key'; +export { default as messageSchema } from './message'; +export { default as pluginAuthSchema } from './pluginAuth'; +export { default as presetSchema } from './preset'; +export { default as projectSchema } from './project'; +export { default as promptSchema } from './prompt'; +export { default as promptGroupSchema } from './promptGroup'; +export { default as roleSchema } from './role'; +export { default as sessionSchema } from './session'; +export { default as shareSchema } from './share'; +export { default as tokenSchema } from './token'; +export { default as toolCallSchema } from './toolCall'; +export { default as transactionSchema } from './transaction'; +export { default as userSchema } from './user'; diff --git a/packages/data-schemas/src/schema/message.ts b/packages/data-schemas/src/schema/message.ts index 2fe471626..4946d1e44 100644 --- a/packages/data-schemas/src/schema/message.ts +++ b/packages/data-schemas/src/schema/message.ts @@ -1,47 +1,6 @@ -import mongoose, { Schema, Document } from 'mongoose'; -import { TFeedbackRating, TFeedbackTag } from 'librechat-data-provider'; - -// @ts-ignore -export interface IMessage extends Document { - messageId: string; - conversationId: string; - user: string; - model?: string; - endpoint?: string; - conversationSignature?: string; - clientId?: string; - invocationId?: number; - parentMessageId?: string; - tokenCount?: number; - summaryTokenCount?: number; - sender?: string; - text?: string; - summary?: string; - isCreatedByUser: boolean; - unfinished?: boolean; - error?: boolean; - finish_reason?: string; - feedback?: { - rating: TFeedbackRating; - tag: TFeedbackTag | undefined; - text?: string; - }; - _meiliIndex?: boolean; - files?: unknown[]; - plugin?: { - latest?: string; - inputs?: unknown[]; - outputs?: string; - }; - plugins?: unknown[]; - content?: unknown[]; - thread_id?: string; - iconURL?: string; - attachments?: unknown[]; - expiredAt?: Date; - createdAt?: Date; - updatedAt?: Date; -} +import mongoose, { Schema } from 'mongoose'; +import type { IMessage } from '~/types/message'; +import mongoMeili from '~/models/plugins/mongoMeili'; const messageSchema: Schema = new Schema( { @@ -207,4 +166,13 @@ messageSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); messageSchema.index({ createdAt: 1 }); messageSchema.index({ messageId: 1, user: 1 }, { unique: true }); +if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { + messageSchema.plugin(mongoMeili, { + host: process.env.MEILI_HOST, + apiKey: process.env.MEILI_MASTER_KEY, + indexName: 'messages', + primaryKey: 'messageId', + }); +} + export default messageSchema; diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index 99a171a78..bd20fa4ca 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -1,36 +1,6 @@ -import { Schema, Document } from 'mongoose'; +import { Schema } from 'mongoose'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; - -export interface IRole extends Document { - name: string; - permissions: { - [PermissionTypes.BOOKMARKS]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.PROMPTS]?: { - [Permissions.SHARED_GLOBAL]?: boolean; - [Permissions.USE]?: boolean; - [Permissions.CREATE]?: boolean; - }; - [PermissionTypes.AGENTS]?: { - [Permissions.SHARED_GLOBAL]?: boolean; - [Permissions.USE]?: boolean; - [Permissions.CREATE]?: boolean; - }; - [PermissionTypes.MULTI_CONVO]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.TEMPORARY_CHAT]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.RUN_CODE]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.WEB_SEARCH]?: { - [Permissions.USE]?: boolean; - }; - }; -} +import type { IRole } from '~/types'; // Create a sub-schema for permissions. Notice we disable _id for this subdocument. const rolePermissionsSchema = new Schema( diff --git a/packages/data-schemas/src/schema/session.ts b/packages/data-schemas/src/schema/session.ts index 558bfd225..9dc2d733a 100644 --- a/packages/data-schemas/src/schema/session.ts +++ b/packages/data-schemas/src/schema/session.ts @@ -1,10 +1,5 @@ -import mongoose, { Schema, Document, Types } from 'mongoose'; - -export interface ISession extends Document { - refreshTokenHash: string; - expiration: Date; - user: Types.ObjectId; -} +import mongoose, { Schema } from 'mongoose'; +import { ISession } from '~/types'; const sessionSchema: Schema = new Schema({ refreshTokenHash: { diff --git a/packages/data-schemas/src/schema/token.ts b/packages/data-schemas/src/schema/token.ts index f23840820..8cb17eec5 100644 --- a/packages/data-schemas/src/schema/token.ts +++ b/packages/data-schemas/src/schema/token.ts @@ -1,15 +1,5 @@ -import { Schema, Document, Types } from 'mongoose'; - -export interface IToken extends Document { - userId: Types.ObjectId; - email?: string; - type?: string; - identifier?: string; - token: string; - createdAt: Date; - expiresAt: Date; - metadata?: Map; -} +import { Schema } from 'mongoose'; +import { IToken } from '~/types'; const tokenSchema: Schema = new Schema({ userId: { diff --git a/packages/data-schemas/src/schema/user.ts b/packages/data-schemas/src/schema/user.ts index 8e5fade2f..6107488ee 100644 --- a/packages/data-schemas/src/schema/user.ts +++ b/packages/data-schemas/src/schema/user.ts @@ -1,39 +1,6 @@ -import { Schema, Document } from 'mongoose'; +import { Schema } from 'mongoose'; import { SystemRoles } from 'librechat-data-provider'; - -export interface IUser extends Document { - name?: string; - username?: string; - email: string; - emailVerified: boolean; - password?: string; - avatar?: string; - provider: string; - role?: string; - googleId?: string; - facebookId?: string; - openidId?: string; - samlId?: string; - ldapId?: string; - githubId?: string; - discordId?: string; - appleId?: string; - plugins?: unknown[]; - twoFactorEnabled?: boolean; - totpSecret?: string; - backupCodes?: Array<{ - codeHash: string; - used: boolean; - usedAt?: Date | null; - }>; - refreshToken?: Array<{ - refreshToken: string; - }>; - expiresAt?: Date; - termsAccepted?: boolean; - createdAt?: Date; - updatedAt?: Date; -} +import { IUser } from '~/types'; // Session sub-schema const SessionSchema = new Schema( @@ -56,7 +23,7 @@ const BackupCodeSchema = new Schema( { _id: false }, ); -const User = new Schema( +const userSchema = new Schema( { name: { type: String, @@ -166,4 +133,4 @@ const User = new Schema( { timestamps: true }, ); -export default User; \ No newline at end of file +export default userSchema; diff --git a/packages/data-schemas/src/types/action.ts b/packages/data-schemas/src/types/action.ts new file mode 100644 index 000000000..6a269856d --- /dev/null +++ b/packages/data-schemas/src/types/action.ts @@ -0,0 +1,28 @@ +import mongoose, { Document } from 'mongoose'; + +export interface IAction extends Document { + user: mongoose.Types.ObjectId; + action_id: string; + type: string; + settings?: unknown; + agent_id?: string; + assistant_id?: string; + metadata: { + api_key?: string; + auth: { + authorization_type?: string; + custom_auth_header?: string; + type: 'service_http' | 'oauth' | 'none'; + authorization_content_type?: string; + authorization_url?: string; + client_url?: string; + scope?: string; + token_exchange_method: 'default_post' | 'basic_auth_header' | null; + }; + domain: string; + privacy_policy_url?: string; + raw_spec?: string; + oauth_client_id?: string; + oauth_client_secret?: string; + }; +} diff --git a/packages/data-schemas/src/types/agent.ts b/packages/data-schemas/src/types/agent.ts new file mode 100644 index 000000000..19b5a04cf --- /dev/null +++ b/packages/data-schemas/src/types/agent.ts @@ -0,0 +1,31 @@ +import { Document, Types } from 'mongoose'; + +export interface IAgent extends Omit { + id: string; + name?: string; + description?: string; + instructions?: string; + avatar?: { + filepath: string; + source: string; + }; + provider: string; + model: string; + model_parameters?: Record; + artifacts?: string; + access_level?: number; + recursion_limit?: number; + tools?: string[]; + tool_kwargs?: Array; + actions?: string[]; + author: Types.ObjectId; + authorName?: string; + hide_sequential_outputs?: boolean; + end_after_tools?: boolean; + agent_ids?: string[]; + isCollaborative?: boolean; + conversation_starters?: string[]; + tool_resources?: unknown; + projectIds?: Types.ObjectId[]; + versions?: Omit[]; +} diff --git a/packages/data-schemas/src/types/assistant.ts b/packages/data-schemas/src/types/assistant.ts new file mode 100644 index 000000000..d2e180c66 --- /dev/null +++ b/packages/data-schemas/src/types/assistant.ts @@ -0,0 +1,15 @@ +import { Document, Types } from 'mongoose'; + +export interface IAssistant extends Document { + user: Types.ObjectId; + assistant_id: string; + avatar?: { + filepath: string; + source: string; + }; + conversation_starters?: string[]; + access_level?: number; + file_ids?: string[]; + actions?: string[]; + append_current_datetime?: boolean; +} diff --git a/packages/data-schemas/src/types/balance.ts b/packages/data-schemas/src/types/balance.ts new file mode 100644 index 000000000..d9497ff51 --- /dev/null +++ b/packages/data-schemas/src/types/balance.ts @@ -0,0 +1,12 @@ +import { Document, Types } from 'mongoose'; + +export interface IBalance extends Document { + user: Types.ObjectId; + tokenCredits: number; + // Automatic refill settings + autoRefillEnabled: boolean; + refillIntervalValue: number; + refillIntervalUnit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months'; + lastRefill: Date; + refillAmount: number; +} diff --git a/packages/data-schemas/src/types/banner.ts b/packages/data-schemas/src/types/banner.ts new file mode 100644 index 000000000..288ae7e9f --- /dev/null +++ b/packages/data-schemas/src/types/banner.ts @@ -0,0 +1,10 @@ +import type { Document } from 'mongoose'; + +export interface IBanner extends Document { + bannerId: string; + message: string; + displayFrom: Date; + displayTo?: Date; + type: 'banner' | 'popup'; + isPublic: boolean; +} diff --git a/packages/data-schemas/src/types/convo.ts b/packages/data-schemas/src/types/convo.ts new file mode 100644 index 000000000..f088db8c9 --- /dev/null +++ b/packages/data-schemas/src/types/convo.ts @@ -0,0 +1,53 @@ +import type { Document, Types } from 'mongoose'; + +// @ts-ignore +export interface IConversation extends Document { + conversationId: string; + title?: string; + user?: string; + messages?: Types.ObjectId[]; + agentOptions?: unknown; + // Fields provided by conversationPreset (adjust types as needed) + endpoint?: string; + endpointType?: string; + model?: string; + region?: string; + chatGptLabel?: string; + examples?: unknown[]; + modelLabel?: string; + promptPrefix?: string; + temperature?: number; + top_p?: number; + topP?: number; + topK?: number; + maxOutputTokens?: number; + maxTokens?: number; + presence_penalty?: number; + frequency_penalty?: number; + file_ids?: string[]; + resendImages?: boolean; + promptCache?: boolean; + thinking?: boolean; + thinkingBudget?: number; + system?: string; + resendFiles?: boolean; + imageDetail?: string; + agent_id?: string; + assistant_id?: string; + instructions?: string; + stop?: string[]; + isArchived?: boolean; + iconURL?: string; + greeting?: string; + spec?: string; + tags?: string[]; + tools?: string[]; + maxContextTokens?: number; + max_tokens?: number; + reasoning_effort?: string; + // Additional fields + files?: string[]; + expiredAt?: Date; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/packages/data-schemas/src/types/file.ts b/packages/data-schemas/src/types/file.ts new file mode 100644 index 000000000..231ab9333 --- /dev/null +++ b/packages/data-schemas/src/types/file.ts @@ -0,0 +1,27 @@ +import { Document, Types } from 'mongoose'; + +export interface IMongoFile extends Omit { + user: Types.ObjectId; + conversationId?: string; + file_id: string; + temp_file_id?: string; + bytes: number; + text?: string; + filename: string; + filepath: string; + object: 'file'; + embedded?: boolean; + type: string; + context?: string; + usage: number; + source: string; + model?: string; + width?: number; + height?: number; + metadata?: { + fileIdentifier?: string; + }; + expiresAt?: Date; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/packages/data-schemas/src/types/index.ts b/packages/data-schemas/src/types/index.ts new file mode 100644 index 000000000..27914d272 --- /dev/null +++ b/packages/data-schemas/src/types/index.ts @@ -0,0 +1,12 @@ +export * from './user'; +export * from './token'; +export * from './convo'; +export * from './session'; +export * from './balance'; +export * from './banner'; +export * from './message'; +export * from './agent'; +export * from './role'; +export * from './action'; +export * from './assistant'; +export * from './file'; diff --git a/packages/data-schemas/src/types/message.ts b/packages/data-schemas/src/types/message.ts new file mode 100644 index 000000000..2743f7242 --- /dev/null +++ b/packages/data-schemas/src/types/message.ts @@ -0,0 +1,44 @@ +import type { Document } from 'mongoose'; +import type { TFeedbackRating, TFeedbackTag } from 'librechat-data-provider'; + +// @ts-ignore +export interface IMessage extends Document { + messageId: string; + conversationId: string; + user: string; + model?: string; + endpoint?: string; + conversationSignature?: string; + clientId?: string; + invocationId?: number; + parentMessageId?: string; + tokenCount?: number; + summaryTokenCount?: number; + sender?: string; + text?: string; + summary?: string; + isCreatedByUser: boolean; + unfinished?: boolean; + error?: boolean; + finish_reason?: string; + feedback?: { + rating: TFeedbackRating; + tag: TFeedbackTag | undefined; + text?: string; + }; + _meiliIndex?: boolean; + files?: unknown[]; + plugin?: { + latest?: string; + inputs?: unknown[]; + outputs?: string; + }; + plugins?: unknown[]; + content?: unknown[]; + thread_id?: string; + iconURL?: string; + attachments?: unknown[]; + expiredAt?: Date; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/packages/data-schemas/src/types/role.ts b/packages/data-schemas/src/types/role.ts new file mode 100644 index 000000000..773a1a392 --- /dev/null +++ b/packages/data-schemas/src/types/role.ts @@ -0,0 +1,33 @@ +import { Document } from 'mongoose'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; + +export interface IRole extends Document { + name: string; + permissions: { + [PermissionTypes.BOOKMARKS]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.PROMPTS]?: { + [Permissions.SHARED_GLOBAL]?: boolean; + [Permissions.USE]?: boolean; + [Permissions.CREATE]?: boolean; + }; + [PermissionTypes.AGENTS]?: { + [Permissions.SHARED_GLOBAL]?: boolean; + [Permissions.USE]?: boolean; + [Permissions.CREATE]?: boolean; + }; + [PermissionTypes.MULTI_CONVO]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.TEMPORARY_CHAT]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.RUN_CODE]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.WEB_SEARCH]?: { + [Permissions.USE]?: boolean; + }; + }; +} diff --git a/packages/data-schemas/src/types/session.ts b/packages/data-schemas/src/types/session.ts new file mode 100644 index 000000000..7df456ac4 --- /dev/null +++ b/packages/data-schemas/src/types/session.ts @@ -0,0 +1,42 @@ +import type { Document, Types } from 'mongoose'; + +export interface ISession extends Document { + refreshTokenHash: string; + expiration: Date; + user: Types.ObjectId; +} + +export interface CreateSessionOptions { + expiration?: Date; +} + +export interface SessionSearchParams { + refreshToken?: string; + userId?: string; + sessionId?: string | { sessionId: string }; +} + +export interface SessionQueryOptions { + lean?: boolean; +} + +export interface DeleteSessionParams { + refreshToken?: string; + sessionId?: string; +} + +export interface DeleteAllSessionsOptions { + excludeCurrentSession?: boolean; + currentSessionId?: string; +} + +export interface SessionResult { + session: Partial; + refreshToken: string; +} + +export interface SignPayloadParams { + payload: Record; + secret?: string; + expirationTime: number; +} diff --git a/packages/data-schemas/src/types/token.ts b/packages/data-schemas/src/types/token.ts new file mode 100644 index 000000000..43fb808eb --- /dev/null +++ b/packages/data-schemas/src/types/token.ts @@ -0,0 +1,42 @@ +import { Document, Types } from 'mongoose'; + +export interface IToken extends Document { + userId: Types.ObjectId; + email?: string; + type?: string; + identifier?: string; + token: string; + createdAt: Date; + expiresAt: Date; + metadata?: Map; +} + +export interface TokenCreateData { + userId: Types.ObjectId | string; + email?: string; + type?: string; + identifier?: string; + token: string; + expiresIn: number; + metadata?: Map; +} + +export interface TokenQuery { + userId?: Types.ObjectId | string; + token?: string; + email?: string; + identifier?: string; +} + +export interface TokenUpdateData { + email?: string; + type?: string; + identifier?: string; + token?: string; + expiresAt?: Date; + metadata?: Map; +} + +export interface TokenDeleteResult { + deletedCount?: number; +} diff --git a/packages/data-schemas/src/types/user.ts b/packages/data-schemas/src/types/user.ts new file mode 100644 index 000000000..206d05181 --- /dev/null +++ b/packages/data-schemas/src/types/user.ts @@ -0,0 +1,72 @@ +import { Document, Types } from 'mongoose'; + +export interface IUser extends Document { + name?: string; + username?: string; + email: string; + emailVerified: boolean; + password?: string; + avatar?: string; + provider: string; + role?: string; + googleId?: string; + facebookId?: string; + openidId?: string; + samlId?: string; + ldapId?: string; + githubId?: string; + discordId?: string; + appleId?: string; + plugins?: unknown[]; + twoFactorEnabled?: boolean; + totpSecret?: string; + backupCodes?: Array<{ + codeHash: string; + used: boolean; + usedAt?: Date | null; + }>; + refreshToken?: Array<{ + refreshToken: string; + }>; + expiresAt?: Date; + termsAccepted?: boolean; + createdAt?: Date; + updatedAt?: Date; +} + +export interface BalanceConfig { + enabled?: boolean; + startBalance?: number; + autoRefillEnabled?: boolean; + refillIntervalValue?: number; + refillIntervalUnit?: string; + refillAmount?: number; +} + +export interface UserCreateData extends Partial { + email: string; +} + +export interface UserUpdateResult { + deletedCount: number; + message: string; +} + +export interface UserSearchCriteria { + email?: string; + username?: string; + googleId?: string; + facebookId?: string; + openidId?: string; + samlId?: string; + ldapId?: string; + githubId?: string; + discordId?: string; + appleId?: string; + _id?: Types.ObjectId | string; +} + +export interface UserQueryOptions { + fieldsToSelect?: string | string[] | null; + lean?: boolean; +} diff --git a/packages/data-schemas/tsconfig.json b/packages/data-schemas/tsconfig.json index 7c5cf16cb..57a321c86 100644 --- a/packages/data-schemas/tsconfig.json +++ b/packages/data-schemas/tsconfig.json @@ -12,7 +12,11 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "sourceMap": true + "sourceMap": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "tests"]