Skip to content

Commit 535e779

Browse files
rubentalstragithub-advanced-security[bot]
authored andcommitted
🚀 feat: Add Cloudflare Turnstile support (#5987)
* 🚀 feat: Add @marsidev/react-turnstile dependency to package.json and package-lock.json * 🚀 feat: Integrate Cloudflare Turnstile configuration support in AppService and add schema validation * 🚀 feat: Implemented Cloudflare Turnstile integration in Login and Registration forms * 🚀 feat: Enhance AppService tests with additional mocks and configuration setups * 🚀 feat: Comment out outdated config version warning tests in AppService.spec.js * 🚀 feat: Remove outdated warning tests and add new checks for environment variables and API health * 🔧 test: Update AppService.spec.js to use expect.anything() for paths validation * 🔧 test: Refactor AppService.spec.js to streamline mocks and enhance clarity * 🔧 chore: removed not needed test * Potential fix for code scanning alert no. 5638: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5629: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5642: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update turnstile.js * Potential fix for code scanning alert no. 5634: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5646: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5647: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 621fa6e commit 535e779

File tree

10 files changed

+145
-11
lines changed

10 files changed

+145
-11
lines changed

api/server/routes/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ router.get('/', async function (req, res) {
7575
process.env.SHOW_BIRTHDAY_ICON === '',
7676
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
7777
interface: req.app.locals.interfaceConfig,
78+
turnstile: req.app.locals.turnstileConfig,
7879
modelSpecs: req.app.locals.modelSpecs,
7980
balance: req.app.locals.balance,
8081
sharedLinksEnabled,

api/server/services/AppService.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const { initializeFirebase } = require('./Files/Firebase/initialize');
1212
const loadCustomConfig = require('./Config/loadCustomConfig');
1313
const handleRateLimits = require('./Config/handleRateLimits');
1414
const { loadDefaultInterface } = require('./start/interface');
15+
const { loadTurnstileConfig } = require('./start/turnstile');
1516
const { azureConfigSetup } = require('./start/azureOpenAI');
1617
const { processModelSpecs } = require('./start/modelSpecs');
1718
const { initializeS3 } = require('./Files/S3/initialize');
@@ -23,7 +24,6 @@ const { getMCPManager } = require('~/config');
2324
const paths = require('~/config/paths');
2425

2526
/**
26-
*
2727
* Loads custom config and initializes app-wide variables.
2828
* @function AppService
2929
* @param {Express.Application} app - The Express application object.
@@ -74,6 +74,7 @@ const AppService = async (app) => {
7474
const socialLogins =
7575
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
7676
const interfaceConfig = await loadDefaultInterface(config, configDefaults);
77+
const turnstileConfig = loadTurnstileConfig(config, configDefaults);
7778

7879
const defaultLocals = {
7980
ocr,
@@ -85,6 +86,7 @@ const AppService = async (app) => {
8586
availableTools,
8687
imageOutputType,
8788
interfaceConfig,
89+
turnstileConfig,
8890
balance,
8991
};
9092

api/server/services/AppService.spec.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ jest.mock('./ToolService', () => ({
4646
},
4747
}),
4848
}));
49+
jest.mock('./start/turnstile', () => ({
50+
loadTurnstileConfig: jest.fn(() => ({
51+
siteKey: 'default-site-key',
52+
options: {},
53+
})),
54+
}));
4955

5056
const azureGroups = [
5157
{
@@ -86,6 +92,10 @@ const azureGroups = [
8692

8793
describe('AppService', () => {
8894
let app;
95+
const mockedTurnstileConfig = {
96+
siteKey: 'default-site-key',
97+
options: {},
98+
};
8999

90100
beforeEach(() => {
91101
app = { locals: {} };
@@ -107,6 +117,7 @@ describe('AppService', () => {
107117
sidePanel: true,
108118
presets: true,
109119
}),
120+
turnstileConfig: mockedTurnstileConfig,
110121
modelSpecs: undefined,
111122
availableTools: {
112123
ExampleTool: {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const { removeNullishValues } = require('librechat-data-provider');
2+
const { logger } = require('~/config');
3+
4+
/**
5+
* Loads and maps the Cloudflare Turnstile configuration.
6+
*
7+
* Expected config structure:
8+
*
9+
* turnstile:
10+
* siteKey: "your-site-key-here"
11+
* options:
12+
* language: "auto" // "auto" or an ISO 639-1 language code (e.g. en)
13+
* size: "normal" // Options: "normal", "compact", "flexible", or "invisible"
14+
*
15+
* @param {TCustomConfig | undefined} config - The loaded custom configuration.
16+
* @param {TConfigDefaults} configDefaults - The custom configuration default values.
17+
* @returns {TCustomConfig['turnstile']} The mapped Turnstile configuration.
18+
*/
19+
function loadTurnstileConfig(config, configDefaults) {
20+
const { turnstile: customTurnstile = {} } = config ?? {};
21+
const { turnstile: defaults = {} } = configDefaults;
22+
23+
/** @type {TCustomConfig['turnstile']} */
24+
const loadedTurnstile = removeNullishValues({
25+
siteKey: customTurnstile.siteKey ?? defaults.siteKey,
26+
options: customTurnstile.options ?? defaults.options,
27+
});
28+
29+
logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2));
30+
return loadedTurnstile;
31+
}
32+
33+
module.exports = {
34+
loadTurnstileConfig,
35+
};

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@dicebear/collection": "^9.2.2",
3535
"@dicebear/core": "^9.2.2",
3636
"@headlessui/react": "^2.1.2",
37+
"@marsidev/react-turnstile": "^1.1.0",
3738
"@radix-ui/react-accordion": "^1.1.2",
3839
"@radix-ui/react-alert-dialog": "^1.0.2",
3940
"@radix-ui/react-checkbox": "^1.0.3",

client/src/components/Auth/LoginForm.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useForm } from 'react-hook-form';
2-
import React, { useState, useEffect } from 'react';
2+
import React, { useState, useEffect, useContext } from 'react';
3+
import { Turnstile } from '@marsidev/react-turnstile';
34
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
45
import type { TAuthContext } from '~/common';
56
import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider';
6-
import { useLocalize } from '~/hooks';
7+
import { ThemeContext, useLocalize } from '~/hooks';
78

89
type TLoginFormProps = {
910
onSubmit: (data: TLoginUser) => void;
@@ -14,16 +15,19 @@ type TLoginFormProps = {
1415

1516
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
1617
const localize = useLocalize();
18+
const { theme } = useContext(ThemeContext);
1719
const {
1820
register,
1921
getValues,
2022
handleSubmit,
2123
formState: { errors },
2224
} = useForm<TLoginUser>();
2325
const [showResendLink, setShowResendLink] = useState<boolean>(false);
26+
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
2427

2528
const { data: config } = useGetStartupConfig();
2629
const useUsernameLogin = config?.ldap?.username;
30+
const validTheme = theme === 'dark' ? 'dark' : 'light';
2731

2832
useEffect(() => {
2933
if (error && error.includes('422') && !showResendLink) {
@@ -159,11 +163,29 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
159163
{localize('com_auth_password_forgot')}
160164
</a>
161165
)}
166+
167+
{/* Render Turnstile only if enabled in startupConfig */}
168+
{startupConfig.turnstile && (
169+
<div className="my-4 flex justify-center">
170+
<Turnstile
171+
siteKey={startupConfig.turnstile.siteKey}
172+
options={{
173+
...startupConfig.turnstile.options,
174+
theme: validTheme,
175+
}}
176+
onSuccess={(token) => setTurnstileToken(token)}
177+
onError={() => setTurnstileToken(null)}
178+
onExpire={() => setTurnstileToken(null)}
179+
/>
180+
</div>
181+
)}
182+
162183
<div className="mt-6">
163184
<button
164185
aria-label={localize('com_auth_continue')}
165186
data-testid="login-button"
166187
type="submit"
188+
disabled={startupConfig.turnstile ? !turnstileToken : false}
167189
className="
168190
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
169191
transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700

client/src/components/Auth/Registration.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { useForm } from 'react-hook-form';
2-
import React, { useState } from 'react';
2+
import React, { useContext, useState } from 'react';
3+
import { Turnstile } from '@marsidev/react-turnstile';
34
import { useNavigate, useOutletContext, useLocation } from 'react-router-dom';
45
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
56
import type { TRegisterUser, TError } from 'librechat-data-provider';
67
import type { TLoginLayoutContext } from '~/common';
78
import { ErrorMessage } from './ErrorMessage';
89
import { Spinner } from '~/components/svg';
9-
import { useLocalize, TranslationKeys } from '~/hooks';
10+
import { useLocalize, TranslationKeys, ThemeContext } from '~/hooks';
1011

1112
const Registration: React.FC = () => {
1213
const navigate = useNavigate();
1314
const localize = useLocalize();
15+
const { theme } = useContext(ThemeContext);
1416
const { startupConfig, startupConfigError, isFetching } = useOutletContext<TLoginLayoutContext>();
1517

1618
const {
@@ -24,10 +26,12 @@ const Registration: React.FC = () => {
2426
const [errorMessage, setErrorMessage] = useState<string>('');
2527
const [isSubmitting, setIsSubmitting] = useState(false);
2628
const [countdown, setCountdown] = useState<number>(3);
29+
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
2730

2831
const location = useLocation();
2932
const queryParams = new URLSearchParams(location.search);
3033
const token = queryParams.get('token');
34+
const validTheme = theme === 'dark' ? 'dark' : 'light';
3135

3236
const registerUser = useRegisterUserMutation({
3337
onMutate: () => {
@@ -178,17 +182,38 @@ const Registration: React.FC = () => {
178182
validate: (value: string) =>
179183
value === password || localize('com_auth_password_not_match'),
180184
})}
185+
186+
{/* Render Turnstile only if enabled in startupConfig */}
187+
{startupConfig?.turnstile && (
188+
<div className="my-4 flex justify-center">
189+
<Turnstile
190+
siteKey={startupConfig.turnstile.siteKey}
191+
options={{
192+
...startupConfig.turnstile.options,
193+
theme: validTheme,
194+
}}
195+
onSuccess={(token) => setTurnstileToken(token)}
196+
onError={() => setTurnstileToken(null)}
197+
onExpire={() => setTurnstileToken(null)}
198+
/>
199+
</div>
200+
)}
201+
181202
<div className="mt-6">
182203
<button
183-
disabled={Object.keys(errors).length > 0}
204+
disabled={
205+
Object.keys(errors).length > 0 ||
206+
isSubmitting ||
207+
(startupConfig?.turnstile ? !turnstileToken : false)
208+
}
184209
type="submit"
185210
aria-label="Submit registration"
186211
className="
187-
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
188-
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
189-
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
190-
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
191-
"
212+
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
213+
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
214+
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
215+
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
216+
"
192217
>
193218
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
194219
</button>

librechat.example.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ interface:
7171
multiConvo: true
7272
agents: true
7373

74+
# Example Cloudflare turnstile (optional)
75+
#turnstile:
76+
# siteKey: "your-site-key-here"
77+
# options:
78+
# language: "auto" # "auto" or an ISO 639-1 language code (e.g. en)
79+
# size: "normal" # Options: "normal", "compact", "flexible", or "invisible"
80+
7481
# Example Registration Object Structure (optional)
7582
registration:
7683
socialLogins: ['github', 'google', 'discord', 'openid', 'facebook', 'apple']

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/data-provider/src/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,10 +505,28 @@ export const intefaceSchema = z
505505
export type TInterfaceConfig = z.infer<typeof intefaceSchema>;
506506
export type TBalanceConfig = z.infer<typeof balanceSchema>;
507507

508+
export const turnstileOptionsSchema = z
509+
.object({
510+
language: z.string().default('auto'),
511+
size: z.enum(['normal', 'compact', 'flexible', 'invisible']).default('normal'),
512+
})
513+
.default({
514+
language: 'auto',
515+
size: 'normal',
516+
});
517+
518+
export const turnstileSchema = z.object({
519+
siteKey: z.string(),
520+
options: turnstileOptionsSchema.optional(),
521+
});
522+
523+
export type TTurnstileConfig = z.infer<typeof turnstileSchema>;
524+
508525
export type TStartupConfig = {
509526
appTitle: string;
510527
socialLogins?: string[];
511528
interface?: TInterfaceConfig;
529+
turnstile?: TTurnstileConfig;
512530
balance?: TBalanceConfig;
513531
discordLoginEnabled: boolean;
514532
facebookLoginEnabled: boolean;
@@ -578,6 +596,7 @@ export const configSchema = z.object({
578596
filteredTools: z.array(z.string()).optional(),
579597
mcpServers: MCPServersSchema.optional(),
580598
interface: intefaceSchema,
599+
turnstile: turnstileSchema.optional(),
581600
fileStrategy: fileSourceSchema.default(FileSources.local),
582601
actions: z
583602
.object({

0 commit comments

Comments
 (0)