diff --git a/api/server/middleware/checkDomainAllowed.js b/api/server/middleware/checkDomainAllowed.js index 914ac44da5..754eb9c127 100644 --- a/api/server/middleware/checkDomainAllowed.js +++ b/api/server/middleware/checkDomainAllowed.js @@ -1,5 +1,5 @@ const { logger } = require('@librechat/data-schemas'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); +const { isEmailDomainAllowed } = require('@librechat/api'); const { getAppConfig } = require('~/server/services/Config'); /** diff --git a/api/server/routes/agents/actions.js b/api/server/routes/agents/actions.js index 2a0629235d..aceaa8d207 100644 --- a/api/server/routes/agents/actions.js +++ b/api/server/routes/agents/actions.js @@ -1,20 +1,19 @@ const express = require('express'); const { nanoid } = require('nanoid'); const { logger } = require('@librechat/data-schemas'); -const { generateCheckAccess } = require('@librechat/api'); +const { generateCheckAccess, isActionDomainAllowed } = require('@librechat/api'); const { Permissions, ResourceType, + PermissionBits, PermissionTypes, actionDelimiter, - PermissionBits, removeNullishValues, } = require('librechat-data-provider'); const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent'); const { updateAction, getActions, deleteAction } = require('~/models/Action'); -const { isActionDomainAllowed } = require('~/server/services/domains'); const { canAccessAgentResource } = require('~/server/middleware'); const { getRoleByName } = require('~/models/Role'); diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index 1853b5c109..3a1a844926 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -1,12 +1,12 @@ const express = require('express'); const { nanoid } = require('nanoid'); const { logger } = require('@librechat/data-schemas'); +const { isActionDomainAllowed } = require('@librechat/api'); const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider'); const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { updateAction, getActions, deleteAction } = require('~/models/Action'); const { updateAssistantDoc, getAssistant } = require('~/models/Assistant'); -const { isActionDomainAllowed } = require('~/server/services/domains'); const router = express.Router(); diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 2ca9aa56b8..1792de66db 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -2,7 +2,7 @@ const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { webcrypto } = require('node:crypto'); const { logger } = require('@librechat/data-schemas'); -const { isEnabled, checkEmailConfig } = require('@librechat/api'); +const { isEnabled, checkEmailConfig, isEmailDomainAllowed } = require('@librechat/api'); const { ErrorTypes, SystemRoles, errorsToString } = require('librechat-data-provider'); const { findUser, @@ -20,7 +20,6 @@ const { deleteUserById, generateRefreshToken, } = require('~/models'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); const { registerSchema } = require('~/strategies/validators'); const { getAppConfig } = require('~/server/services/Config'); const { sendEmail } = require('~/server/utils'); diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 5245b43221..4b9708861c 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1,7 +1,12 @@ const { sleep } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); const { tool: toolFn, DynamicStructuredTool } = require('@langchain/core/tools'); -const { getToolkitKey, hasCustomUserVars, getUserMCPAuthMap } = require('@librechat/api'); +const { + getToolkitKey, + hasCustomUserVars, + getUserMCPAuthMap, + isActionDomainAllowed, +} = require('@librechat/api'); const { Tools, Constants, @@ -26,7 +31,6 @@ const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/p const { getEndpointsConfig, getCachedTools } = require('~/server/services/Config'); const { manifestToolMap, toolkits } = require('~/app/clients/tools/manifest'); const { createOnSearchResults } = require('~/server/services/Tools/search'); -const { isActionDomainAllowed } = require('~/server/services/domains'); const { recordUsage } = require('~/server/services/Threads'); const { loadTools } = require('~/app/clients/tools/util'); const { redactMessage } = require('~/config/parsers'); diff --git a/api/strategies/ldapStrategy.js b/api/strategies/ldapStrategy.js index 17d54df4a9..dcadc26a45 100644 --- a/api/strategies/ldapStrategy.js +++ b/api/strategies/ldapStrategy.js @@ -1,10 +1,9 @@ const fs = require('fs'); const LdapStrategy = require('passport-ldapauth'); const { logger } = require('@librechat/data-schemas'); -const { isEnabled, getBalanceConfig } = require('@librechat/api'); const { SystemRoles, ErrorTypes } = require('librechat-data-provider'); +const { isEnabled, getBalanceConfig, isEmailDomainAllowed } = require('@librechat/api'); const { createUser, findUser, updateUser, countUsers } = require('~/models'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); const { getAppConfig } = require('~/server/services/Config'); const { diff --git a/api/strategies/ldapStrategy.spec.js b/api/strategies/ldapStrategy.spec.js index d3d51a4cda..a00e9b14b7 100644 --- a/api/strategies/ldapStrategy.spec.js +++ b/api/strategies/ldapStrategy.spec.js @@ -11,6 +11,7 @@ jest.mock('@librechat/data-schemas', () => ({ jest.mock('@librechat/api', () => ({ // isEnabled used for TLS flags isEnabled: jest.fn(() => false), + isEmailDomainAllowed: jest.fn(() => true), getBalanceConfig: jest.fn(() => ({ enabled: false })), })); @@ -25,10 +26,6 @@ jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn().mockResolvedValue({}), })); -jest.mock('~/server/services/domains', () => ({ - isEmailDomainAllowed: jest.fn(() => true), -})); - // Mock passport-ldapauth to capture verify callback let verifyCallback; jest.mock('passport-ldapauth', () => { @@ -39,8 +36,8 @@ jest.mock('passport-ldapauth', () => { }); const { ErrorTypes } = require('librechat-data-provider'); +const { isEmailDomainAllowed } = require('@librechat/api'); const { findUser, createUser, updateUser, countUsers } = require('~/models'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); // Helper to call the verify callback and wrap in a Promise for convenience const callVerify = (userinfo) => diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 0a396211cf..ce564fc655 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -13,9 +13,9 @@ const { safeStringify, findOpenIDUser, getBalanceConfig, + isEmailDomainAllowed, } = require('@librechat/api'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); const { findUser, createUser, updateUser } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); const getLogStores = require('~/cache/getLogStores'); diff --git a/api/strategies/samlStrategy.js b/api/strategies/samlStrategy.js index a5f2e1d0bd..843baf8a64 100644 --- a/api/strategies/samlStrategy.js +++ b/api/strategies/samlStrategy.js @@ -2,12 +2,11 @@ const fs = require('fs'); const path = require('path'); const fetch = require('node-fetch'); const passport = require('passport'); -const { getBalanceConfig } = require('@librechat/api'); const { ErrorTypes } = require('librechat-data-provider'); const { hashToken, logger } = require('@librechat/data-schemas'); const { Strategy: SamlStrategy } = require('@node-saml/passport-saml'); +const { getBalanceConfig, isEmailDomainAllowed } = require('@librechat/api'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); const { findUser, createUser, updateUser } = require('~/models'); const { getAppConfig } = require('~/server/services/Config'); const paths = require('~/config/paths'); diff --git a/api/strategies/samlStrategy.spec.js b/api/strategies/samlStrategy.spec.js index 812c24f26f..06c969ce46 100644 --- a/api/strategies/samlStrategy.spec.js +++ b/api/strategies/samlStrategy.spec.js @@ -26,6 +26,7 @@ jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn().mockResolvedValue({}), })); jest.mock('@librechat/api', () => ({ + isEmailDomainAllowed: jest.fn(() => true), getBalanceConfig: jest.fn(() => ({ tokenCredits: 1000, startBalance: 1000, diff --git a/api/strategies/socialLogin.js b/api/strategies/socialLogin.js index 5f7fab01c4..bad70cc040 100644 --- a/api/strategies/socialLogin.js +++ b/api/strategies/socialLogin.js @@ -1,8 +1,7 @@ -const { isEnabled } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { ErrorTypes } = require('librechat-data-provider'); +const { isEnabled, isEmailDomainAllowed } = require('@librechat/api'); const { createSocialUser, handleExistingUser } = require('./process'); -const { isEmailDomainAllowed } = require('~/server/services/domains'); const { getAppConfig } = require('~/server/services/Config'); const { findUser } = require('~/models'); diff --git a/api/server/services/domains.spec.js b/packages/api/src/auth/domain.spec.ts similarity index 73% rename from api/server/services/domains.spec.js rename to packages/api/src/auth/domain.spec.ts index a640628462..c84d32cbcd 100644 --- a/api/server/services/domains.spec.js +++ b/packages/api/src/auth/domain.spec.ts @@ -1,9 +1,5 @@ -const { isEmailDomainAllowed, isActionDomainAllowed } = require('~/server/services/domains'); -const { getAppConfig } = require('~/server/services/Config'); - -jest.mock('~/server/services/Config', () => ({ - getAppConfig: jest.fn(), -})); +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { isEmailDomainAllowed, isActionDomainAllowed } from './domain'; describe('isEmailDomainAllowed', () => { afterEach(() => { @@ -24,39 +20,72 @@ describe('isEmailDomainAllowed', () => { it('should return true if customConfig is not available', async () => { const email = 'test@domain1.com'; - getAppConfig.mockResolvedValue(null); const result = isEmailDomainAllowed(email, null); expect(result).toBe(true); }); it('should return true if allowedDomains is not defined in customConfig', async () => { const email = 'test@domain1.com'; - getAppConfig.mockResolvedValue({}); const result = isEmailDomainAllowed(email, undefined); expect(result).toBe(true); }); it('should return true if domain is included in the allowedDomains', async () => { const email = 'user@domain1.com'; - getAppConfig.mockResolvedValue({ - registration: { - allowedDomains: ['domain1.com', 'domain2.com'], - }, - }); const result = isEmailDomainAllowed(email, ['domain1.com', 'domain2.com']); expect(result).toBe(true); }); it('should return false if domain is not included in the allowedDomains', async () => { const email = 'user@domain3.com'; - getAppConfig.mockResolvedValue({ - registration: { - allowedDomains: ['domain1.com', 'domain2.com'], - }, - }); const result = isEmailDomainAllowed(email, ['domain1.com', 'domain2.com']); expect(result).toBe(false); }); + + describe('case-insensitive domain matching', () => { + it('should match domains case-insensitively when email has uppercase domain', () => { + const email = 'user@DOMAIN1.COM'; + const result = isEmailDomainAllowed(email, ['domain1.com', 'domain2.com']); + expect(result).toBe(true); + }); + + it('should match domains case-insensitively when allowedDomains has uppercase', () => { + const email = 'user@domain1.com'; + const result = isEmailDomainAllowed(email, ['DOMAIN1.COM', 'DOMAIN2.COM']); + expect(result).toBe(true); + }); + + it('should match domains with mixed case in email', () => { + const email = 'user@Example.Com'; + const result = isEmailDomainAllowed(email, ['example.com', 'domain2.com']); + expect(result).toBe(true); + }); + + it('should match domains with mixed case in allowedDomains', () => { + const email = 'user@example.com'; + const result = isEmailDomainAllowed(email, ['Example.Com', 'Domain2.Com']); + expect(result).toBe(true); + }); + + it('should match when both email and allowedDomains have different mixed cases', () => { + const email = 'user@ExAmPlE.cOm'; + const result = isEmailDomainAllowed(email, ['eXaMpLe.CoM', 'domain2.com']); + expect(result).toBe(true); + }); + + it('should still return false for non-matching domains regardless of case', () => { + const email = 'user@DOMAIN3.COM'; + const result = isEmailDomainAllowed(email, ['domain1.com', 'DOMAIN2.COM']); + expect(result).toBe(false); + }); + + it('should handle null/undefined entries in allowedDomains gracefully', () => { + const email = 'user@domain1.com'; + // @ts-expect-error Testing invalid input + const result = isEmailDomainAllowed(email, [null, 'DOMAIN1.COM', undefined]); + expect(result).toBe(true); + }); + }); }); describe('isActionDomainAllowed', () => { @@ -74,15 +103,15 @@ describe('isActionDomainAllowed', () => { }); it('should return false for non-string inputs', async () => { + /** @ts-expect-error */ expect(await isActionDomainAllowed(123)).toBe(false); + /** @ts-expect-error */ expect(await isActionDomainAllowed({})).toBe(false); + /** @ts-expect-error */ expect(await isActionDomainAllowed([])).toBe(false); }); it('should return false for invalid domain formats', async () => { - getAppConfig.mockResolvedValue({ - actions: { allowedDomains: ['http://', 'https://'] }, - }); expect(await isActionDomainAllowed('http://', ['http://', 'https://'])).toBe(false); expect(await isActionDomainAllowed('https://', ['http://', 'https://'])).toBe(false); }); @@ -91,19 +120,14 @@ describe('isActionDomainAllowed', () => { // Configuration Tests describe('configuration handling', () => { it('should return true if customConfig is null', async () => { - getAppConfig.mockResolvedValue(null); expect(await isActionDomainAllowed('example.com', null)).toBe(true); }); it('should return true if actions.allowedDomains is not defined', async () => { - getAppConfig.mockResolvedValue({}); expect(await isActionDomainAllowed('example.com', undefined)).toBe(true); }); it('should return true if allowedDomains is empty array', async () => { - getAppConfig.mockResolvedValue({ - actions: { allowedDomains: [] }, - }); expect(await isActionDomainAllowed('example.com', [])).toBe(true); }); }); @@ -118,14 +142,6 @@ describe('isActionDomainAllowed', () => { 'swapi.dev', ]; - beforeEach(() => { - getAppConfig.mockResolvedValue({ - actions: { - allowedDomains, - }, - }); - }); - it('should match exact domains', async () => { expect(await isActionDomainAllowed('example.com', allowedDomains)).toBe(true); expect(await isActionDomainAllowed('other.com', allowedDomains)).toBe(false); @@ -159,14 +175,6 @@ describe('isActionDomainAllowed', () => { describe('edge cases', () => { const edgeAllowedDomains = ['example.com', '*.test.com']; - beforeEach(() => { - getAppConfig.mockResolvedValue({ - actions: { - allowedDomains: edgeAllowedDomains, - }, - }); - }); - it('should handle domains with query parameters', async () => { expect(await isActionDomainAllowed('example.com?param=value', edgeAllowedDomains)).toBe(true); }); @@ -186,12 +194,9 @@ describe('isActionDomainAllowed', () => { it('should handle invalid entries in allowedDomains', async () => { const invalidAllowedDomains = ['example.com', null, undefined, '', 'test.com']; - getAppConfig.mockResolvedValue({ - actions: { - allowedDomains: invalidAllowedDomains, - }, - }); + /** @ts-expect-error */ expect(await isActionDomainAllowed('example.com', invalidAllowedDomains)).toBe(true); + /** @ts-expect-error */ expect(await isActionDomainAllowed('test.com', invalidAllowedDomains)).toBe(true); }); }); diff --git a/api/server/services/domains.js b/packages/api/src/auth/domain.ts similarity index 76% rename from api/server/services/domains.js rename to packages/api/src/auth/domain.ts index 67966eeaac..06710ac6d7 100644 --- a/api/server/services/domains.js +++ b/packages/api/src/auth/domain.ts @@ -1,14 +1,13 @@ /** - * @param {string} email - * @param {string[]} [allowedDomains] - * @returns {boolean} + * @param email + * @param allowedDomains */ -function isEmailDomainAllowed(email, allowedDomains) { +export function isEmailDomainAllowed(email: string, allowedDomains?: string[] | null): boolean { if (!email) { return false; } - const domain = email.split('@')[1]; + const domain = email.split('@')[1]?.toLowerCase(); if (!domain) { return false; @@ -20,21 +19,15 @@ function isEmailDomainAllowed(email, allowedDomains) { return true; } - return allowedDomains.includes(domain); + return allowedDomains.some((allowedDomain) => allowedDomain?.toLowerCase() === domain); } -/** - * Normalizes a domain string - * @param {string} domain - * @returns {string|null} - */ /** * Normalizes a domain string. If the domain is invalid, returns null. * Normalized === lowercase, trimmed, and protocol added if missing. - * @param {string} domain - * @returns {string|null} + * @param domain */ -function normalizeDomain(domain) { +function normalizeDomain(domain: string): string | null { try { let normalizedDomain = domain.toLowerCase().trim(); @@ -62,11 +55,13 @@ function normalizeDomain(domain) { /** * Checks if the given domain is allowed. If no restrictions are set, allows all domains. - * @param {string} [domain] - * @param {string[]} [allowedDomains] - * @returns {Promise} + * @param domain + * @param allowedDomains */ -async function isActionDomainAllowed(domain, allowedDomains) { +export async function isActionDomainAllowed( + domain?: string | null, + allowedDomains?: string[] | null, +): Promise { if (!domain || typeof domain !== 'string') { return false; } @@ -101,5 +96,3 @@ async function isActionDomainAllowed(domain, allowedDomains) { return false; } - -module.exports = { isEmailDomainAllowed, isActionDomainAllowed }; diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts index a5fb25405a..bee8cf1691 100644 --- a/packages/api/src/auth/index.ts +++ b/packages/api/src/auth/index.ts @@ -1 +1,2 @@ +export * from './domain'; export * from './openid';