diff --git a/.env.example b/.env.example index 5f0e40ac3..d7f651c8d 100644 --- a/.env.example +++ b/.env.example @@ -374,6 +374,7 @@ LDAP_BIND_CREDENTIALS= LDAP_USER_SEARCH_BASE= LDAP_SEARCH_FILTER=mail={{username}} LDAP_CA_CERT_PATH= +# LDAP_LOGIN_USES_USERNAME=true # LDAP_ID= # LDAP_USERNAME= # LDAP_FULL_NAME= diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js index 338651752..a19919b31 100644 --- a/api/server/routes/__tests__/config.spec.js +++ b/api/server/routes/__tests__/config.spec.js @@ -76,7 +76,9 @@ describe.skip('GET /', () => { openidLoginEnabled: true, openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', - ldapLoginEnabled: true, + ldap: { + enabled: true, + }, serverDomain: 'http://test-server.com', emailLoginEnabled: 'true', registrationEnabled: 'true', diff --git a/api/server/routes/__tests__/ldap.spec.js b/api/server/routes/__tests__/ldap.spec.js new file mode 100644 index 000000000..6e0a95bfe --- /dev/null +++ b/api/server/routes/__tests__/ldap.spec.js @@ -0,0 +1,55 @@ +const request = require('supertest'); +const express = require('express'); +const { getLdapConfig } = require('~/server/services/Config/ldap'); +const { isEnabled } = require('~/server/utils'); + +jest.mock('~/server/services/Config/ldap'); +jest.mock('~/server/utils'); + +const app = express(); + +// Mock the route handler +app.get('/api/config', (req, res) => { + const ldapConfig = getLdapConfig(); + res.json({ ldap: ldapConfig }); +}); + +describe('LDAP Config Tests', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return LDAP config with username property when LDAP_LOGIN_USES_USERNAME is enabled', async () => { + getLdapConfig.mockReturnValue({ enabled: true, username: true }); + isEnabled.mockReturnValue(true); + + const response = await request(app).get('/api/config'); + + expect(response.statusCode).toBe(200); + expect(response.body.ldap).toEqual({ + enabled: true, + username: true, + }); + }); + + it('should return LDAP config without username property when LDAP_LOGIN_USES_USERNAME is not enabled', async () => { + getLdapConfig.mockReturnValue({ enabled: true }); + isEnabled.mockReturnValue(false); + + const response = await request(app).get('/api/config'); + + expect(response.statusCode).toBe(200); + expect(response.body.ldap).toEqual({ + enabled: true, + }); + }); + + it('should not return LDAP config when LDAP is not enabled', async () => { + getLdapConfig.mockReturnValue(undefined); + + const response = await request(app).get('/api/config'); + + expect(response.statusCode).toBe(200); + expect(response.body.ldap).toBeUndefined(); + }); +}); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 113395939..3fc90c14b 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -1,5 +1,6 @@ const express = require('express'); const { CacheKeys, defaultSocialLogins } = require('librechat-data-provider'); +const { getLdapConfig } = require('~/server/services/Config/ldap'); const { getProjectByName } = require('~/models/Project'); const { isEnabled } = require('~/server/utils'); const { getLogStores } = require('~/cache'); @@ -33,7 +34,8 @@ router.get('/', async function (req, res) { const instanceProject = await getProjectByName('instance', '_id'); - const ldapLoginEnabled = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE; + const ldap = getLdapConfig(); + try { /** @type {TStartupConfig} */ const payload = { @@ -51,10 +53,9 @@ router.get('/', async function (req, res) { !!process.env.OPENID_SESSION_SECRET, openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID', openidImageUrl: process.env.OPENID_IMAGE_URL, - ldapLoginEnabled, serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080', emailLoginEnabled, - registrationEnabled: !ldapLoginEnabled && isEnabled(process.env.ALLOW_REGISTRATION), + registrationEnabled: !ldap?.enabled && isEnabled(process.env.ALLOW_REGISTRATION), socialLoginEnabled: isEnabled(process.env.ALLOW_SOCIAL_LOGIN), emailEnabled: (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) && @@ -76,6 +77,10 @@ router.get('/', async function (req, res) { instanceProjectId: instanceProject._id.toString(), }; + if (ldap) { + payload.ldap = ldap; + } + if (typeof process.env.CUSTOM_FOOTER === 'string') { payload.customFooter = process.env.CUSTOM_FOOTER; } diff --git a/api/server/services/Config/ldap.js b/api/server/services/Config/ldap.js new file mode 100644 index 000000000..1a4cfbed0 --- /dev/null +++ b/api/server/services/Config/ldap.js @@ -0,0 +1,22 @@ +const { isEnabled } = require('~/server/utils'); + +/** @returns {TStartupConfig['ldap'] | undefined} */ +const getLdapConfig = () => { + const ldapLoginEnabled = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE; + + const ldap = { + enabled: ldapLoginEnabled, + }; + const ldapLoginUsesUsername = isEnabled(process.env.LDAP_LOGIN_USES_USERNAME); + if (!ldapLoginEnabled) { + return ldap; + } + + if (ldapLoginUsesUsername) { + ldap.username = true; + } +}; + +module.exports = { + getLdapConfig, +}; diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index c3e156f25..3e50e9257 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -1,5 +1,6 @@ import { useForm } from 'react-hook-form'; import React, { useState, useEffect } from 'react'; +import { useGetStartupConfig } from 'librechat-data-provider/react-query'; import type { TLoginUser, TStartupConfig } from 'librechat-data-provider'; import type { TAuthContext } from '~/common'; import { useResendVerificationEmail } from '~/data-provider'; @@ -22,6 +23,9 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, } = useForm(); const [showResendLink, setShowResendLink] = useState(false); + const { data: config } = useGetStartupConfig(); + const useUsernameLogin = config?.ldap?.username; + useEffect(() => { if (error && error.includes('422') && !showResendLink) { setShowResendLink(true); @@ -82,12 +86,15 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, = ({ onSubmit, startupConfig, error, htmlFor="email" className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" > - {localize('com_auth_email_address')} + {useUsernameLogin + ? localize('com_auth_username').replace(/ \(.*$/, '') + : localize('com_auth_email_address')} {renderError('email')} diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx index 263db278d..71c3d6592 100644 --- a/client/src/components/Auth/__tests__/Login.spec.tsx +++ b/client/src/components/Auth/__tests__/Login.spec.tsx @@ -21,7 +21,9 @@ const mockStartupConfig = { openidLoginEnabled: true, openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', - ldapLoginEnabled: false, + ldap: { + enabled: false, + }, registrationEnabled: true, emailLoginEnabled: true, socialLoginEnabled: true, diff --git a/client/src/components/Auth/__tests__/LoginForm.spec.tsx b/client/src/components/Auth/__tests__/LoginForm.spec.tsx index 0005450f6..d30b709eb 100644 --- a/client/src/components/Auth/__tests__/LoginForm.spec.tsx +++ b/client/src/components/Auth/__tests__/LoginForm.spec.tsx @@ -23,7 +23,9 @@ const mockStartupConfig: TStartupConfig = { passwordResetEnabled: true, serverDomain: 'mock-server', appTitle: '', - ldapLoginEnabled: false, + ldap: { + enabled: false, + }, emailEnabled: false, checkBalance: false, showBirthdayIcon: false, diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 967e3de4a..8abb8002f 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -294,7 +294,13 @@ export type TStartupConfig = { openidLoginEnabled: boolean; openidLabel: string; openidImageUrl: string; - ldapLoginEnabled: boolean; + /** LDAP Auth Configuration */ + ldap?: { + /** LDAP enabled */ + enabled: boolean; + /** Whether LDAP uses username vs. email */ + username?: boolean; + }; serverDomain: string; emailLoginEnabled: boolean; registrationEnabled: boolean;