mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
🔒 feat: Add Content Security Policy using Helmet middleware (#7377)
* 🔒 feat: Add Content Security Policy using Helmet middleware * 🔒 feat: Set trust proxy and refine Content Security Policy directives * 🎨 feat: add `copy-tex` to improve copying KaTeX (#7308) When selecting equations and using copy paste, uses the correct latex code. Co-authored-by: Ruben Talstra <RubenTalstra1211@outlook.com> * 🔃 refactor: `AgentFooter` to conditionally render buttons based on `activePanel` (#7306) * 🚀 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> * 🔒 feat: Refactor Content Security Policy setup to use Helmet middleware with custom directives * 🔒 feat: Enhance Content Security Policy to include Sandpack Bundler URL * 🔒 feat: Update Content Security Policy and integrate Turnstile captcha support --------- Co-authored-by: andresgit <9771158+andresgit@users.noreply.github.com> Co-authored-by: matt burnett <mawburn@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
parent
fe311df969
commit
7a91f6ca62
6 changed files with 93 additions and 60 deletions
|
|
@ -71,6 +71,7 @@
|
||||||
"firebase": "^11.0.2",
|
"firebase": "^11.0.2",
|
||||||
"googleapis": "^126.0.1",
|
"googleapis": "^126.0.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ require('dotenv').config();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
require('module-alias')({ base: path.resolve(__dirname, '..') });
|
require('module-alias')({ base: path.resolve(__dirname, '..') });
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
const helmet = require('helmet');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
|
|
@ -22,7 +23,15 @@ const staticCache = require('./utils/staticCache');
|
||||||
const noIndex = require('./middleware/noIndex');
|
const noIndex = require('./middleware/noIndex');
|
||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
|
|
||||||
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
const {
|
||||||
|
PORT,
|
||||||
|
HOST,
|
||||||
|
ALLOW_SOCIAL_LOGIN,
|
||||||
|
DISABLE_COMPRESSION,
|
||||||
|
TRUST_PROXY,
|
||||||
|
SANDPACK_BUNDLER_URL,
|
||||||
|
SANDPACK_STATIC_BUNDLER_URL,
|
||||||
|
} = process.env ?? {};
|
||||||
|
|
||||||
const port = Number(PORT) || 3080;
|
const port = Number(PORT) || 3080;
|
||||||
const host = HOST || 'localhost';
|
const host = HOST || 'localhost';
|
||||||
|
|
@ -38,6 +47,8 @@ const startServer = async () => {
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
|
app.set('trust proxy', trusted_proxy);
|
||||||
|
|
||||||
await AppService(app);
|
await AppService(app);
|
||||||
|
|
||||||
const indexPath = path.join(app.locals.paths.dist, 'index.html');
|
const indexPath = path.join(app.locals.paths.dist, 'index.html');
|
||||||
|
|
@ -49,23 +60,54 @@ const startServer = async () => {
|
||||||
app.use(noIndex);
|
app.use(noIndex);
|
||||||
app.use(errorController);
|
app.use(errorController);
|
||||||
app.use(express.json({ limit: '3mb' }));
|
app.use(express.json({ limit: '3mb' }));
|
||||||
app.use(mongoSanitize());
|
|
||||||
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
|
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
|
||||||
app.use(staticCache(app.locals.paths.dist));
|
app.use(mongoSanitize());
|
||||||
app.use(staticCache(app.locals.paths.fonts));
|
|
||||||
app.use(staticCache(app.locals.paths.assets));
|
|
||||||
app.set('trust proxy', trusted_proxy);
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
useDefaults: false,
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", 'https://challenges.cloudflare.com'],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
fontSrc: ["'self'", 'data:'],
|
||||||
|
objectSrc: ["'none'"],
|
||||||
|
imgSrc: ["'self'", 'data:'],
|
||||||
|
mediaSrc: ["'self'", 'data:', 'blob:'],
|
||||||
|
connectSrc: ["'self'"],
|
||||||
|
frameSrc: [
|
||||||
|
"'self'",
|
||||||
|
'https://challenges.cloudflare.com',
|
||||||
|
'https://codesandbox.io',
|
||||||
|
...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []),
|
||||||
|
...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []),
|
||||||
|
],
|
||||||
|
frameAncestors: [
|
||||||
|
"'self'",
|
||||||
|
'https://codesandbox.io',
|
||||||
|
...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []),
|
||||||
|
...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (!isEnabled(DISABLE_COMPRESSION)) {
|
if (!isEnabled(DISABLE_COMPRESSION)) {
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
} else {
|
||||||
|
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve static assets with aggressive caching
|
||||||
|
app.use(staticCache(app.locals.paths.dist));
|
||||||
|
app.use(staticCache(app.locals.paths.fonts));
|
||||||
|
app.use(staticCache(app.locals.paths.assets));
|
||||||
|
|
||||||
if (!ALLOW_SOCIAL_LOGIN) {
|
if (!ALLOW_SOCIAL_LOGIN) {
|
||||||
console.warn(
|
console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.');
|
||||||
'Social logins are disabled. Set Environment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* OAUTH */
|
/* OAUTH */
|
||||||
|
|
@ -128,7 +170,7 @@ const startServer = async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(port, host, () => {
|
app.listen(port, host, () => {
|
||||||
if (host == '0.0.0.0') {
|
if (host === '0.0.0.0') {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`,
|
`Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,16 @@ function loadTurnstileConfig(config, configDefaults) {
|
||||||
options: customTurnstile.options ?? defaults.options,
|
options: customTurnstile.options ?? defaults.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2));
|
const enabled = Boolean(loadedTurnstile.siteKey);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
logger.info(
|
||||||
|
'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.info('Turnstile is DISABLED (no siteKey provided).');
|
||||||
|
}
|
||||||
|
|
||||||
return loadedTurnstile;
|
return loadedTurnstile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type TLoginFormProps = {
|
||||||
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
|
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { theme } = useContext(ThemeContext);
|
const { theme } = useContext(ThemeContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
getValues,
|
getValues,
|
||||||
|
|
@ -28,6 +29,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
const { data: config } = useGetStartupConfig();
|
const { data: config } = useGetStartupConfig();
|
||||||
const useUsernameLogin = config?.ldap?.username;
|
const useUsernameLogin = config?.ldap?.username;
|
||||||
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
||||||
|
const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error && error.includes('422') && !showResendLink) {
|
if (error && error.includes('422') && !showResendLink) {
|
||||||
|
|
@ -100,20 +102,12 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.email}
|
aria-invalid={!!errors.email}
|
||||||
className="
|
className="peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary transition-colors duration-200 focus:border-green-500 focus:outline-none"
|
||||||
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
|
|
||||||
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
|
|
||||||
"
|
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="
|
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500"
|
||||||
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
|
|
||||||
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
|
|
||||||
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
|
|
||||||
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{useUsernameLogin
|
{useUsernameLogin
|
||||||
? localize('com_auth_username').replace(/ \(.*$/, '')
|
? localize('com_auth_username').replace(/ \(.*$/, '')
|
||||||
|
|
@ -135,20 +129,12 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.password}
|
aria-invalid={!!errors.password}
|
||||||
className="
|
className="peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary transition-colors duration-200 focus:border-green-500 focus:outline-none"
|
||||||
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
|
|
||||||
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
|
|
||||||
"
|
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="
|
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500"
|
||||||
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
|
|
||||||
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
|
|
||||||
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
|
|
||||||
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{localize('com_auth_password')}
|
{localize('com_auth_password')}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -164,16 +150,15 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Render Turnstile only if enabled in startupConfig */}
|
{requireCaptcha && (
|
||||||
{startupConfig.turnstile && (
|
|
||||||
<div className="my-4 flex justify-center">
|
<div className="my-4 flex justify-center">
|
||||||
<Turnstile
|
<Turnstile
|
||||||
siteKey={startupConfig.turnstile.siteKey}
|
siteKey={startupConfig.turnstile!.siteKey}
|
||||||
options={{
|
options={{
|
||||||
...startupConfig.turnstile.options,
|
...startupConfig.turnstile!.options,
|
||||||
theme: validTheme,
|
theme: validTheme,
|
||||||
}}
|
}}
|
||||||
onSuccess={(token) => setTurnstileToken(token)}
|
onSuccess={setTurnstileToken}
|
||||||
onError={() => setTurnstileToken(null)}
|
onError={() => setTurnstileToken(null)}
|
||||||
onExpire={() => setTurnstileToken(null)}
|
onExpire={() => setTurnstileToken(null)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -185,11 +170,8 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
aria-label={localize('com_auth_continue')}
|
aria-label={localize('com_auth_continue')}
|
||||||
data-testid="login-button"
|
data-testid="login-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={startupConfig.turnstile ? !turnstileToken : false}
|
disabled={requireCaptcha && !turnstileToken}
|
||||||
className="
|
className="w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:opacity-50 disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
|
||||||
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
|
||||||
transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ const Registration: React.FC = () => {
|
||||||
const token = queryParams.get('token');
|
const token = queryParams.get('token');
|
||||||
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
||||||
|
|
||||||
|
// only require captcha if we have a siteKey
|
||||||
|
const requireCaptcha = Boolean(startupConfig?.turnstile?.siteKey);
|
||||||
|
|
||||||
const registerUser = useRegisterUserMutation({
|
const registerUser = useRegisterUserMutation({
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
@ -73,21 +76,13 @@ const Registration: React.FC = () => {
|
||||||
validation,
|
validation,
|
||||||
)}
|
)}
|
||||||
aria-invalid={!!errors[id]}
|
aria-invalid={!!errors[id]}
|
||||||
className="
|
className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none"
|
||||||
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
|
|
||||||
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
|
|
||||||
"
|
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
data-testid={id}
|
data-testid={id}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className="
|
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
|
||||||
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
|
|
||||||
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
|
|
||||||
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
|
|
||||||
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{localize(label)}
|
{localize(label)}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -183,8 +178,7 @@ const Registration: React.FC = () => {
|
||||||
value === password || localize('com_auth_password_not_match'),
|
value === password || localize('com_auth_password_not_match'),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Render Turnstile only if enabled in startupConfig */}
|
{startupConfig?.turnstile?.siteKey && (
|
||||||
{startupConfig?.turnstile && (
|
|
||||||
<div className="my-4 flex justify-center">
|
<div className="my-4 flex justify-center">
|
||||||
<Turnstile
|
<Turnstile
|
||||||
siteKey={startupConfig.turnstile.siteKey}
|
siteKey={startupConfig.turnstile.siteKey}
|
||||||
|
|
@ -204,16 +198,11 @@ const Registration: React.FC = () => {
|
||||||
disabled={
|
disabled={
|
||||||
Object.keys(errors).length > 0 ||
|
Object.keys(errors).length > 0 ||
|
||||||
isSubmitting ||
|
isSubmitting ||
|
||||||
(startupConfig?.turnstile ? !turnstileToken : false)
|
(requireCaptcha && !turnstileToken)
|
||||||
}
|
}
|
||||||
type="submit"
|
type="submit"
|
||||||
aria-label="Submit registration"
|
aria-label="Submit registration"
|
||||||
className="
|
className="w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50 disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
|
||||||
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
|
||||||
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
|
||||||
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
|
||||||
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
|
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -87,6 +87,7 @@
|
||||||
"firebase": "^11.0.2",
|
"firebase": "^11.0.2",
|
||||||
"googleapis": "^126.0.1",
|
"googleapis": "^126.0.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|
@ -31510,6 +31511,15 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/helmet": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/highlight.js": {
|
"node_modules/highlight.js": {
|
||||||
"version": "11.8.0",
|
"version": "11.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue