🔒 feat: password reset disable option; fix: account email error message (#2327)

* feat: password reset  disable option; fix: account email leak

* fix(LoginSpec): typo

* test: fixed LoginForm test

* fix: disable password reset when undefined

* refactor: use a helper function

* fix: tests

* feat: Remove unused error message in password reset process

* chore: Update password reset email message

* refactor: only allow password reset if explicitly allowed

* feat: Add password reset email service configuration check

The code changes in `checks.js` add a new function `checkPasswordReset()` that checks if the email service is configured when password reset is enabled. If the email service is not configured, a warning message is logged. This change ensures secure password reset functionality by prompting the user to configure the email service.

Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>

* chore: remove import order rules

* refactor: simplify password reset logic and align against Observable Response Discrepancy

* chore: make password reset warning more prominent

* chore(AuthService): better logging for password resets, refactor requestPasswordReset to use req object, fix sendEmail error when email config is not present

* refactor: fix styling of password reset email message

* chore: add missing type for passwordResetEnabled, TStartupConfig

* fix(LoginForm): prevent login form flickering

* fix(ci): Update login form to use mocked startupConfig for rendering correctly

* refactor: Improve password reset UI, applies DRY

* chore: Add logging to password reset validation middleware

* chore(CONTRIBUTING): Update import order conventions

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
This commit is contained in:
Marco Beretta 2024-06-06 17:39:36 +02:00 committed by GitHub
parent a7f5b57272
commit 5452d4c20c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 288 additions and 137 deletions

View file

@ -319,6 +319,7 @@ ALLOW_EMAIL_LOGIN=true
ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
SESSION_EXPIRY=1000 * 60 * 15

View file

@ -67,39 +67,6 @@ module.exports = {
'react/display-name': ['off'],
'no-unused-vars': ['error', { varsIgnorePattern: '^_' }],
quotes: ['error', 'single'],
'import/order': [
'warn',
{
groups: [
['builtin'], // Node.js standard libraries, react
['external'], // npm packages
['type'], // Type imports (TypeScript)
[
'internal', // Internal alias imports eg.(~/Component)
'parent', // Parent directory imports eg.(../ParentComponent)
'sibling', // Sibling imports eg.(./components/MyComponent)
'index',
'object',
],
],
// 'newlines-between': 'always', // Enforce new lines between groups
pathGroups: [
{
pattern: '{react,react-dom/**}',
group: 'builtin',
position: 'before',
},
{
pattern: '~/**',
group: 'internal',
position: 'before',
},
],
pathGroupsExcludedImportTypes: ['builtin', 'type'], // Exclude these types from the path group rule
warnOnUnassignedImports: true, // Warn for unassigned imports
// alphabetize: { order: 'asc', caseInsensitive: true }, // Alphabetize imports within each group
},
],
},
overrides: [
{

View file

@ -126,6 +126,18 @@ Apply the following naming conventions to branches, labels, and other Git-relate
- **Current Stance**: At present, this backend transition is of lower priority and might not be pursued.
## 7. Module Import Conventions
- `npm` packages first,
- from shortest line (top) to longest (bottom)
- Followed by typescript types (pertains to data-provider and client workspaces)
- longest line (top) to shortest (bottom)
- types from package come first
- Lastly, local imports
- longest line (top) to shortest (bottom)
- imports with alias `~` treated the same as relative import with respect to line length
---

View file

@ -39,7 +39,7 @@ const getUserController = async (req, res) => {
const resetPasswordRequestController = async (req, res) => {
try {
const resetService = await requestPasswordReset(req.body.email);
const resetService = await requestPasswordReset(req);
if (resetService instanceof Error) {
return res.status(400).json(resetService);
} else {

View file

@ -17,6 +17,7 @@ const validateMessageReq = require('./validateMessageReq');
const buildEndpointOption = require('./buildEndpointOption');
const validateRegistration = require('./validateRegistration');
const validateImageRequest = require('./validateImageRequest');
const validatePasswordReset = require('./validatePasswordReset');
const moderateText = require('./moderateText');
const noIndex = require('./noIndex');
const importLimiters = require('./importLimiters');
@ -40,6 +41,7 @@ module.exports = {
buildEndpointOption,
validateRegistration,
validateImageRequest,
validatePasswordReset,
validateModel,
moderateText,
noIndex,

View file

@ -0,0 +1,13 @@
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
function validatePasswordReset(req, res, next) {
if (isEnabled(process.env.ALLOW_PASSWORD_RESET)) {
next();
} else {
logger.warn(`Password reset attempt while not allowed. IP: ${req.ip}`);
res.status(403).send('Password reset is not allowed.');
}
}
module.exports = validatePasswordReset;

View file

@ -1,6 +1,7 @@
const { isEnabled } = require('~/server/utils');
function validateRegistration(req, res, next) {
const setting = process.env.ALLOW_REGISTRATION?.toLowerCase();
if (setting === 'true') {
if (isEnabled(process.env.ALLOW_REGISTRATION)) {
next();
} else {
res.status(403).send('Registration is not allowed.');

View file

@ -25,6 +25,7 @@ afterEach(() => {
delete process.env.DOMAIN_SERVER;
delete process.env.ALLOW_REGISTRATION;
delete process.env.ALLOW_SOCIAL_LOGIN;
delete process.env.ALLOW_PASSWORD_RESET;
delete process.env.LDAP_URL;
delete process.env.LDAP_BIND_DN;
delete process.env.LDAP_BIND_CREDENTIALS;
@ -55,6 +56,7 @@ describe.skip('GET /', () => {
process.env.DOMAIN_SERVER = 'http://test-server.com';
process.env.ALLOW_REGISTRATION = 'true';
process.env.ALLOW_SOCIAL_LOGIN = 'true';
process.env.ALLOW_PASSWORD_RESET = 'true';
process.env.LDAP_URL = 'Test LDAP URL';
process.env.LDAP_BIND_DN = 'Test LDAP Bind DN';
process.env.LDAP_BIND_CREDENTIALS = 'Test LDAP Bind Credentials';
@ -78,6 +80,7 @@ describe.skip('GET /', () => {
serverDomain: 'http://test-server.com',
emailLoginEnabled: 'true',
registrationEnabled: 'true',
passwordResetEnabled: 'true',
socialLoginEnabled: 'true',
});
});

View file

@ -15,6 +15,7 @@ const {
requireLdapAuth,
requireLocalAuth,
validateRegistration,
validatePasswordReset,
} = require('../middleware');
const router = express.Router();
@ -32,7 +33,12 @@ router.post(
);
router.post('/refresh', refreshController);
router.post('/register', registerLimiter, checkBan, validateRegistration, registrationController);
router.post('/requestPasswordReset', resetPasswordRequestController);
router.post('/resetPassword', resetPasswordController);
router.post(
'/requestPasswordReset',
checkBan,
validatePasswordReset,
resetPasswordRequestController,
);
router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController);
module.exports = router;

View file

@ -6,6 +6,7 @@ const { logger } = require('~/config');
const router = express.Router();
const emailLoginEnabled =
process.env.ALLOW_EMAIL_LOGIN === undefined || isEnabled(process.env.ALLOW_EMAIL_LOGIN);
const passwordResetEnabled = isEnabled(process.env.ALLOW_PASSWORD_RESET);
router.get('/', async function (req, res) {
const isBirthday = () => {
@ -42,6 +43,7 @@ router.get('/', async function (req, res) {
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM,
passwordResetEnabled,
checkBalance: isEnabled(process.env.CHECK_BALANCE),
showBirthdayIcon:
isBirthday() ||

View file

@ -16,6 +16,19 @@ const domains = {
const isProduction = process.env.NODE_ENV === 'production';
/**
* Check if email configuration is set
* @returns {Boolean}
*/
function checkEmailConfig() {
return (
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM
);
}
/**
* Logout user
*
@ -114,14 +127,20 @@ const registerUser = async (user) => {
/**
* Request password reset
*
* @param {String} email
* @returns
* @param {Express.Request} req
*/
const requestPasswordReset = async (email) => {
const requestPasswordReset = async (req) => {
const { email } = req.body;
const user = await User.findOne({ email }).lean();
const emailEnabled = checkEmailConfig();
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
if (!user) {
return new Error('Email does not exist');
logger.warn(`[requestPasswordReset] [No user found] [Email: ${email}] [IP: ${req.ip}]`);
return {
message: 'If an account with that email exists, a password reset link has been sent to it.',
};
}
let token = await Token.findOne({ userId: user._id });
@ -140,12 +159,6 @@ const requestPasswordReset = async (email) => {
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`;
const emailEnabled =
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM;
if (emailEnabled) {
sendEmail(
user.email,
@ -158,10 +171,19 @@ const requestPasswordReset = async (email) => {
},
'requestPasswordReset.handlebars',
);
return { link: '' };
logger.info(
`[requestPasswordReset] Link emailed. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`,
);
} else {
logger.info(
`[requestPasswordReset] Link issued. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`,
);
return { link };
}
return {
message: 'If an account with that email exists, a password reset link has been sent to it.',
};
};
/**
@ -190,7 +212,9 @@ const resetPassword = async (userId, token, password) => {
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true });
const user = await User.findById({ _id: userId });
const emailEnabled = checkEmailConfig();
if (emailEnabled) {
sendEmail(
user.email,
'Password Reset Successfully',
@ -201,9 +225,10 @@ const resetPassword = async (userId, token, password) => {
},
'passwordReset.handlebars',
);
}
await passwordResetToken.deleteOne();
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
return { message: 'Password reset was successful' };
};

View file

@ -3,6 +3,7 @@ const {
deprecatedAzureVariables,
conflictingAzureVariables,
} = require('librechat-data-provider');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const secretDefaults = {
@ -49,6 +50,8 @@ function checkVariables() {
Please use the config (\`librechat.yaml\`) file for setting up OpenRouter, and use \`OPENROUTER_KEY\` or another environment variable instead.`,
);
}
checkPasswordReset();
}
/**
@ -107,4 +110,30 @@ Latest version: ${Constants.CONFIG_VERSION}
}
}
function checkPasswordReset() {
const emailEnabled =
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM;
const passwordResetAllowed = isEnabled(process.env.ALLOW_PASSWORD_RESET);
if (!emailEnabled && passwordResetAllowed) {
logger.warn(
`❗❗❗
Password reset is enabled with \`ALLOW_PASSWORD_RESET\` but email service is not configured.
This setup is insecure as password reset links will be issued with a recognized email.
Please configure email service for secure password reset functionality.
https://www.librechat.ai/docs/configuration/authentication/password_reset
`,
);
}
}
module.exports = { checkVariables, checkHealth, checkConfig, checkAzureVariables };

View file

@ -14,7 +14,9 @@ function Login() {
return (
<>
{error && <ErrorMessage>{localize(getLoginError(error))}</ErrorMessage>}
{startupConfig?.emailLoginEnabled && <LoginForm onSubmit={login} />}
{startupConfig?.emailLoginEnabled && (
<LoginForm onSubmit={login} startupConfig={startupConfig} />
)}
{startupConfig?.registrationEnabled && (
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{' '}

View file

@ -1,19 +1,23 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
import { TLoginUser } from 'librechat-data-provider';
type TLoginFormProps = {
onSubmit: (data: TLoginUser) => void;
startupConfig: TStartupConfig;
};
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig }) => {
const localize = useLocalize();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<TLoginUser>();
if (!startupConfig) {
return null;
}
const renderError = (fieldName: string) => {
const errorMessage = errors[fieldName]?.message;
@ -81,9 +85,11 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
</div>
{renderError('password')}
</div>
{startupConfig.passwordResetEnabled && (
<a href="/forgot-password" className="text-sm text-green-500">
{localize('com_auth_password_forgot')}
</a>
)}
<div className="mt-6">
<button
aria-label="Sign in"

View file

@ -1,11 +1,37 @@
import { useForm } from 'react-hook-form';
import { useState, useEffect } from 'react';
import { useState, ReactNode } from 'react';
import { useOutletContext } from 'react-router-dom';
import { useRequestPasswordResetMutation } from 'librechat-data-provider/react-query';
import type { TRequestPasswordReset, TRequestPasswordResetResponse } from 'librechat-data-provider';
import type { FC } from 'react';
import type { TLoginLayoutContext } from '~/common';
import { useLocalize } from '~/hooks';
const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
return (
<div
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700 dark:bg-green-900 dark:text-white"
role="alert"
>
{children}
</div>
);
};
const ResetPasswordBodyText = () => {
const localize = useLocalize();
return (
<div className="flex flex-col">
{localize('com_auth_reset_password_if_email_exists')}
<span>
<a className="text-sm text-green-500 hover:underline" href="/login">
{localize('com_auth_back_to_login')}
</a>
</span>
</div>
);
};
function RequestPasswordReset() {
const localize = useLocalize();
const {
@ -13,72 +39,39 @@ function RequestPasswordReset() {
handleSubmit,
formState: { errors },
} = useForm<TRequestPasswordReset>();
const [resetLink, setResetLink] = useState<string | undefined>(undefined);
const [bodyText, setBodyText] = useState<React.ReactNode | undefined>(undefined);
const { startupConfig, setError, setHeaderText } = useOutletContext<TLoginLayoutContext>();
const [bodyText, setBodyText] = useState<ReactNode | undefined>(undefined);
const { startupConfig, setHeaderText } = useOutletContext<TLoginLayoutContext>();
const requestPasswordReset = useRequestPasswordResetMutation();
const onSubmit = (data: TRequestPasswordReset) => {
requestPasswordReset.mutate(data, {
onSuccess: (data: TRequestPasswordResetResponse) => {
if (!startupConfig?.emailEnabled) {
setResetLink(data.link);
}
},
onError: () => {
setError('com_auth_error_reset_password');
setTimeout(() => {
setError(null);
}, 5000);
},
});
};
useEffect(() => {
if (bodyText) {
return;
}
if (!requestPasswordReset.isSuccess) {
setHeaderText('com_auth_reset_password');
setBodyText(undefined);
return;
}
if (startupConfig?.emailEnabled) {
setHeaderText('com_auth_reset_password_link_sent');
setBodyText(localize('com_auth_reset_password_email_sent'));
return;
}
if (data.link && !startupConfig?.emailEnabled) {
setHeaderText('com_auth_reset_password');
setBodyText(
<span>
{localize('com_auth_click')}{' '}
<a className="text-green-500 hover:underline" href={resetLink}>
<a className="text-green-500 hover:underline" href={data.link}>
{localize('com_auth_here')}
</a>{' '}
{localize('com_auth_to_reset_your_password')}
</span>,
);
}, [
requestPasswordReset.isSuccess,
startupConfig?.emailEnabled,
resetLink,
localize,
setHeaderText,
bodyText,
]);
} else {
setHeaderText('com_auth_reset_password_link_sent');
setBodyText(<ResetPasswordBodyText />);
}
},
onError: () => {
setHeaderText('com_auth_reset_password_link_sent');
setBodyText(<ResetPasswordBodyText />);
},
});
};
if (bodyText) {
return (
<div
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700 dark:bg-green-900 dark:text-white"
role="alert"
>
{bodyText}
</div>
);
return <BodyTextWrapper>{bodyText}</BodyTextWrapper>;
}
return (

View file

@ -51,7 +51,7 @@ const setup = ({
user: {},
},
},
useGetStartupCongfigReturnValue = mockStartupConfig,
useGetStartupConfigReturnValue = mockStartupConfig,
} = {}) => {
const mockUseLoginUser = jest
.spyOn(mockDataProvider, 'useLoginUserMutation')
@ -64,18 +64,18 @@ const setup = ({
const mockUseGetStartupConfig = jest
.spyOn(mockDataProvider, 'useGetStartupConfig')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetStartupCongfigReturnValue);
.mockReturnValue(useGetStartupConfigReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({
startupConfig: useGetStartupCongfigReturnValue.data,
startupConfig: useGetStartupConfigReturnValue.data,
});
const renderResult = render(
<AuthLayout
startupConfig={useGetStartupCongfigReturnValue.data as TStartupConfig}
isFetching={useGetStartupCongfigReturnValue.isFetching}
startupConfig={useGetStartupConfigReturnValue.data as TStartupConfig}
isFetching={useGetStartupConfigReturnValue.isFetching}
error={null}
startupConfigError={null}
header={'Welcome back'}
@ -161,7 +161,7 @@ test('Navigates to / on successful login', async () => {
isError: false,
isSuccess: true,
},
useGetStartupCongfigReturnValue: {
useGetStartupConfigReturnValue: {
...mockStartupConfig,
data: {
...mockStartupConfig.data,

View file

@ -1,17 +1,103 @@
import { render } from 'test/layout-test-utils';
import userEvent from '@testing-library/user-event';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
import Login from '../LoginForm';
jest.mock('librechat-data-provider/react-query');
const mockLogin = jest.fn();
const mockStartupConfig: TStartupConfig = {
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
googleLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
registrationEnabled: true,
emailLoginEnabled: true,
socialLoginEnabled: true,
passwordResetEnabled: true,
serverDomain: 'mock-server',
appTitle: '',
ldapLoginEnabled: false,
emailEnabled: false,
checkBalance: false,
showBirthdayIcon: false,
helpAndFaqURL: '',
};
const setup = ({
useGetUserQueryReturnValue = {
isLoading: false,
isError: false,
data: {},
},
useLoginUserReturnValue = {
isLoading: false,
isError: false,
mutate: jest.fn(),
data: {},
isSuccess: false,
},
useRefreshTokenMutationReturnValue = {
isLoading: false,
isError: false,
mutate: jest.fn(),
data: {
token: 'mock-token',
user: {},
},
},
useGetStartupConfigReturnValue = {
isLoading: false,
isError: false,
data: mockStartupConfig,
},
} = {}) => {
const mockUseLoginUser = jest
.spyOn(mockDataProvider, 'useLoginUserMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useLoginUserReturnValue);
const mockUseGetUserQuery = jest
.spyOn(mockDataProvider, 'useGetUserQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetUserQueryReturnValue);
const mockUseGetStartupConfig = jest
.spyOn(mockDataProvider, 'useGetStartupConfig')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetStartupConfigReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
return {
mockUseLoginUser,
mockUseGetUserQuery,
mockUseGetStartupConfig,
mockUseRefreshTokenMutation,
};
};
beforeEach(() => {
setup();
});
test('renders login form', () => {
const { getByLabelText } = render(<Login onSubmit={mockLogin} />);
const { getByLabelText } = render(
<Login onSubmit={mockLogin} startupConfig={mockStartupConfig} />,
);
expect(getByLabelText(/email/i)).toBeInTheDocument();
expect(getByLabelText(/password/i)).toBeInTheDocument();
});
test('submits login form', async () => {
const { getByLabelText, getByRole } = render(<Login onSubmit={mockLogin} />);
const { getByLabelText, getByRole } = render(
<Login onSubmit={mockLogin} startupConfig={mockStartupConfig} />,
);
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
@ -24,7 +110,9 @@ test('submits login form', async () => {
});
test('displays validation error messages', async () => {
const { getByLabelText, getByRole, getByText } = render(<Login onSubmit={mockLogin} />);
const { getByLabelText, getByRole, getByText } = render(
<Login onSubmit={mockLogin} startupConfig={mockStartupConfig} />,
);
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });

View file

@ -50,7 +50,7 @@ const setup = ({
user: {},
},
},
useGetStartupCongfigReturnValue = mockStartupConfig,
useGetStartupConfigReturnValue = mockStartupConfig,
} = {}) => {
const mockUseRegisterUserMutation = jest
.spyOn(mockDataProvider, 'useRegisterUserMutation')
@ -63,18 +63,18 @@ const setup = ({
const mockUseGetStartupConfig = jest
.spyOn(mockDataProvider, 'useGetStartupConfig')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetStartupCongfigReturnValue);
.mockReturnValue(useGetStartupConfigReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({
startupConfig: useGetStartupCongfigReturnValue.data,
startupConfig: useGetStartupConfigReturnValue.data,
});
const renderResult = render(
<AuthLayout
startupConfig={useGetStartupCongfigReturnValue.data as TStartupConfig}
isFetching={useGetStartupCongfigReturnValue.isFetching}
startupConfig={useGetStartupConfigReturnValue.data as TStartupConfig}
isFetching={useGetStartupConfigReturnValue.isFetching}
error={null}
startupConfigError={null}
header={'Create your account'}

View file

@ -293,10 +293,10 @@ export default {
com_auth_here: 'HERE',
com_auth_to_reset_your_password: 'to reset your password.',
com_auth_reset_password_link_sent: 'Email Sent',
com_auth_reset_password_if_email_exists:
'If an account with that email exists, an email with password reset instructions has been sent. Please make sure to check your spam folder.',
com_auth_reset_password_email_sent:
'An email has been sent to you with further instructions to reset your password.',
com_auth_error_reset_password:
'There was a problem resetting your password. There was no user found with the email address provided. Please try again.',
'If the user is registered, an email will be sent to the inbox.',
com_auth_reset_password_success: 'Password Reset Success',
com_auth_login_with_new_password: 'You may now login with your new password.',
com_auth_error_invalid_reset_token: 'This password reset token is no longer valid.',

View file

@ -276,6 +276,7 @@ export type TStartupConfig = {
emailLoginEnabled: boolean;
registrationEnabled: boolean;
socialLoginEnabled: boolean;
passwordResetEnabled: boolean;
emailEnabled: boolean;
checkBalance: boolean;
showBirthdayIcon: boolean;