mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
♻️ refactor: Login and Registration component Improvement (#2716)
* ♻️ refactor: Login form improvement * display error message when API is down * add loading animation to Login form while fetching data * optimize startupConfig to fetch data only on initial render to prevent unnecessary API calls * 🚑 fix: clear authentication error messages on successful login * ♻️ refactor: componentize duplicate codes on registration and login screens * chore: update types * refactor: layout rendering order * refactor: startup title fix * refactor: reset/request-reset-password under new AuthLayout * ci: fix Login.spec.ts * ci: fix registration.spec.tsx --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
2b7a973a33
commit
9f2538fcd9
19 changed files with 775 additions and 750 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
TLoginUser,
|
||||
AuthTypeEnum,
|
||||
TConversation,
|
||||
TStartupConfig,
|
||||
EModelEndpoint,
|
||||
AssistantsEndpoint,
|
||||
AuthorizationTypeEnum,
|
||||
|
|
@ -390,3 +391,13 @@ export interface SwitcherProps {
|
|||
endpointKeyProvided: boolean;
|
||||
isCollapsed: boolean;
|
||||
}
|
||||
|
||||
export type TLoginLayoutContext = {
|
||||
startupConfig: TStartupConfig | null;
|
||||
startupConfigError: unknown;
|
||||
isFetching: boolean;
|
||||
error: string | null;
|
||||
setError: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
headerText: string;
|
||||
setHeaderText: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
|
|
|||
90
client/src/components/Auth/AuthLayout.tsx
Normal file
90
client/src/components/Auth/AuthLayout.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { ThemeSelector } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { BlinkAnimation } from './BlinkAnimation';
|
||||
import { TStartupConfig } from 'librechat-data-provider';
|
||||
import SocialLoginRender from './SocialLoginRender';
|
||||
import Footer from './Footer';
|
||||
|
||||
const ErrorRender = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className="mt-16 flex justify-center">
|
||||
<div
|
||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||
role="alert"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function AuthLayout({
|
||||
children,
|
||||
header,
|
||||
isFetching,
|
||||
startupConfig,
|
||||
startupConfigError,
|
||||
pathname,
|
||||
error,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
header: React.ReactNode;
|
||||
isFetching: boolean;
|
||||
startupConfig: TStartupConfig | null | undefined;
|
||||
startupConfigError: unknown | null | undefined;
|
||||
pathname: string;
|
||||
error: string | null;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const DisplayError = () => {
|
||||
if (startupConfigError !== null && startupConfigError !== undefined) {
|
||||
return <ErrorRender>{localize('com_auth_error_login_server')}</ErrorRender>;
|
||||
} else if (error === 'com_auth_error_invalid_reset_token') {
|
||||
return (
|
||||
<ErrorRender>
|
||||
{localize('com_auth_error_invalid_reset_token')}{' '}
|
||||
<a className="font-semibold text-green-600 hover:underline" href="/forgot-password">
|
||||
{localize('com_auth_click_here')}
|
||||
</a>{' '}
|
||||
{localize('com_auth_to_try_again')}
|
||||
</ErrorRender>
|
||||
);
|
||||
} else if (error) {
|
||||
return <ErrorRender>{localize(error)}</ErrorRender>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
|
||||
<BlinkAnimation active={isFetching}>
|
||||
<div className="mt-12 h-24 w-full bg-cover">
|
||||
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
|
||||
</div>
|
||||
</BlinkAnimation>
|
||||
<DisplayError />
|
||||
<div className="absolute bottom-0 left-0 md:m-4">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow items-center justify-center">
|
||||
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||
{!startupConfigError && !isFetching && (
|
||||
<h1
|
||||
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{header}
|
||||
</h1>
|
||||
)}
|
||||
{children}
|
||||
{(pathname.includes('login') || pathname.includes('register')) && (
|
||||
<SocialLoginRender startupConfig={startupConfig} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Footer startupConfig={startupConfig} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthLayout;
|
||||
29
client/src/components/Auth/BlinkAnimation.tsx
Normal file
29
client/src/components/Auth/BlinkAnimation.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export const BlinkAnimation = ({
|
||||
active,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const style = `
|
||||
@keyframes blink-animation {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}`;
|
||||
|
||||
if (!active) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{style}</style>
|
||||
<div style={{ animation: 'blink-animation 3s infinite' }}>{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
8
client/src/components/Auth/ErrorMessage.tsx
Normal file
8
client/src/components/Auth/ErrorMessage.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
|
||||
<div
|
||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||
role="alert"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
45
client/src/components/Auth/Footer.tsx
Normal file
45
client/src/components/Auth/Footer.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useLocalize } from '~/hooks';
|
||||
import { TStartupConfig } from 'librechat-data-provider';
|
||||
|
||||
function Footer({ startupConfig }: { startupConfig: TStartupConfig | null | undefined }) {
|
||||
const localize = useLocalize();
|
||||
if (!startupConfig) {
|
||||
return null;
|
||||
}
|
||||
const privacyPolicy = startupConfig.interface?.privacyPolicy;
|
||||
const termsOfService = startupConfig.interface?.termsOfService;
|
||||
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={privacyPolicy.externalUrl}
|
||||
target={privacyPolicy.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</a>
|
||||
);
|
||||
|
||||
const termsOfServiceRender = termsOfService?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={termsOfService.externalUrl}
|
||||
target={termsOfService.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="align-end m-4 flex justify-center gap-2">
|
||||
{privacyPolicyRender}
|
||||
{privacyPolicyRender && termsOfServiceRender && (
|
||||
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
{termsOfServiceRender}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1,182 +1,30 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { ThemeSelector } from '~/components/ui';
|
||||
import SocialButton from './SocialButton';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
|
||||
import { getLoginError } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import LoginForm from './LoginForm';
|
||||
|
||||
function Login() {
|
||||
const { login, error, isAuthenticated } = useAuthContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
if (!startupConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const socialLogins = startupConfig.socialLogins ?? [];
|
||||
|
||||
const providerComponents = {
|
||||
discord: (
|
||||
<SocialButton
|
||||
key="discord"
|
||||
enabled={startupConfig.discordLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="discord"
|
||||
Icon={DiscordIcon}
|
||||
label={localize('com_auth_discord_login')}
|
||||
id="discord"
|
||||
/>
|
||||
),
|
||||
facebook: (
|
||||
<SocialButton
|
||||
key="facebook"
|
||||
enabled={startupConfig.facebookLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="facebook"
|
||||
Icon={FacebookIcon}
|
||||
label={localize('com_auth_facebook_login')}
|
||||
id="facebook"
|
||||
/>
|
||||
),
|
||||
github: (
|
||||
<SocialButton
|
||||
key="github"
|
||||
enabled={startupConfig.githubLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="github"
|
||||
Icon={GithubIcon}
|
||||
label={localize('com_auth_github_login')}
|
||||
id="github"
|
||||
/>
|
||||
),
|
||||
google: (
|
||||
<SocialButton
|
||||
key="google"
|
||||
enabled={startupConfig.googleLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="google"
|
||||
Icon={GoogleIcon}
|
||||
label={localize('com_auth_google_login')}
|
||||
id="google"
|
||||
/>
|
||||
),
|
||||
openid: (
|
||||
<SocialButton
|
||||
key="openid"
|
||||
enabled={startupConfig.openidLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="openid"
|
||||
Icon={() =>
|
||||
startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)
|
||||
}
|
||||
label={startupConfig.openidLabel}
|
||||
id="openid"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const privacyPolicy = startupConfig.interface?.privacyPolicy;
|
||||
const termsOfService = startupConfig.interface?.termsOfService;
|
||||
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={privacyPolicy.externalUrl}
|
||||
target={privacyPolicy.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</a>
|
||||
);
|
||||
|
||||
const termsOfServiceRender = termsOfService?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={termsOfService.externalUrl}
|
||||
target={termsOfService.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
</a>
|
||||
);
|
||||
const { error, login } = useAuthContext();
|
||||
const { startupConfig } = useOutletContext<TLoginLayoutContext>();
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
|
||||
<div className="mt-12 h-24 w-full bg-cover">
|
||||
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 md:m-4">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center">
|
||||
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||
<h1
|
||||
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{localize('com_auth_welcome_back')}
|
||||
</h1>
|
||||
{error && (
|
||||
<div
|
||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||
role="alert"
|
||||
>
|
||||
{localize(getLoginError(error))}
|
||||
</div>
|
||||
)}
|
||||
{startupConfig.emailLoginEnabled && <LoginForm onSubmit={login} />}
|
||||
{startupConfig.registrationEnabled && (
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
||||
{' '}
|
||||
{localize('com_auth_no_account')}{' '}
|
||||
<a href="/register" className="p-1 text-green-500">
|
||||
{localize('com_auth_sign_up')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{startupConfig.socialLoginEnabled && (
|
||||
<>
|
||||
{startupConfig.emailLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t border-gray-300 uppercase dark:border-gray-600">
|
||||
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
|
||||
Or
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8" />
|
||||
</>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
{socialLogins.map((provider) => providerComponents[provider] || null)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="align-end m-4 flex justify-center gap-2">
|
||||
{privacyPolicyRender}
|
||||
{privacyPolicyRender && termsOfServiceRender && (
|
||||
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
{termsOfServiceRender}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{error && <ErrorMessage>{localize(getLoginError(error))}</ErrorMessage>}
|
||||
{startupConfig?.emailLoginEnabled && <LoginForm onSubmit={login} />}
|
||||
{startupConfig?.registrationEnabled && (
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
||||
{' '}
|
||||
{localize('com_auth_no_account')}{' '}
|
||||
<a href="/register" className="p-1 text-green-500">
|
||||
{localize('com_auth_sign_up')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRegisterUserMutation, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type { TRegisterUser } from 'librechat-data-provider';
|
||||
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
||||
import { ThemeSelector } from '~/components/ui';
|
||||
import SocialButton from './SocialButton';
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TRegisterUser, TError } from 'librechat-data-provider';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { ErrorMessage } from './ErrorMessage';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const Registration: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const localize = useLocalize();
|
||||
const { startupConfig, startupConfigError, isFetching } = useOutletContext<TLoginLayoutContext>();
|
||||
|
||||
const {
|
||||
register,
|
||||
|
|
@ -31,10 +30,8 @@ const Registration: React.FC = () => {
|
|||
navigate('/c/new');
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
//@ts-ignore - error is of type unknown
|
||||
if (error.response?.data?.message) {
|
||||
//@ts-ignore - error is of type unknown
|
||||
setErrorMessage(error.response?.data?.message);
|
||||
if ((error as TError).response?.data?.message) {
|
||||
setErrorMessage((error as TError).response?.data?.message ?? '');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -45,12 +42,6 @@ const Registration: React.FC = () => {
|
|||
}
|
||||
}, [startupConfig, navigate]);
|
||||
|
||||
if (!startupConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const socialLogins = startupConfig.socialLogins ?? [];
|
||||
|
||||
const renderInput = (id: string, label: string, type: string, validation: object) => (
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
|
|
@ -67,7 +58,7 @@ const Registration: React.FC = () => {
|
|||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
data-testid={id}
|
||||
></input>
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
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"
|
||||
|
|
@ -83,120 +74,16 @@ const Registration: React.FC = () => {
|
|||
</div>
|
||||
);
|
||||
|
||||
const providerComponents = {
|
||||
discord: (
|
||||
<SocialButton
|
||||
key="discord"
|
||||
enabled={startupConfig.discordLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="discord"
|
||||
Icon={DiscordIcon}
|
||||
label={localize('com_auth_discord_login')}
|
||||
id="discord"
|
||||
/>
|
||||
),
|
||||
facebook: (
|
||||
<SocialButton
|
||||
key="facebook"
|
||||
enabled={startupConfig.facebookLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="facebook"
|
||||
Icon={FacebookIcon}
|
||||
label={localize('com_auth_facebook_login')}
|
||||
id="facebook"
|
||||
/>
|
||||
),
|
||||
github: (
|
||||
<SocialButton
|
||||
key="github"
|
||||
enabled={startupConfig.githubLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="github"
|
||||
Icon={GithubIcon}
|
||||
label={localize('com_auth_github_login')}
|
||||
id="github"
|
||||
/>
|
||||
),
|
||||
google: (
|
||||
<SocialButton
|
||||
key="google"
|
||||
enabled={startupConfig.googleLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="google"
|
||||
Icon={GoogleIcon}
|
||||
label={localize('com_auth_google_login')}
|
||||
id="google"
|
||||
/>
|
||||
),
|
||||
openid: (
|
||||
<SocialButton
|
||||
key="openid"
|
||||
enabled={startupConfig.openidLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="openid"
|
||||
Icon={() =>
|
||||
startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)
|
||||
}
|
||||
label={startupConfig.openidLabel}
|
||||
id="openid"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const privacyPolicy = startupConfig.interface?.privacyPolicy;
|
||||
const termsOfService = startupConfig.interface?.termsOfService;
|
||||
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={privacyPolicy.externalUrl}
|
||||
target={privacyPolicy.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</a>
|
||||
);
|
||||
|
||||
const termsOfServiceRender = termsOfService?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={termsOfService.externalUrl}
|
||||
target={termsOfService.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
|
||||
<div className="mt-12 h-24 w-full bg-cover">
|
||||
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 md:m-4">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center">
|
||||
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||
<h1
|
||||
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{localize('com_auth_create_account')}
|
||||
</h1>
|
||||
{error && (
|
||||
<div
|
||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||
role="alert"
|
||||
data-testid="registration-error"
|
||||
>
|
||||
{localize('com_auth_error_create')} {errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
{error && (
|
||||
<ErrorMessage>
|
||||
{localize('com_auth_error_create')} {errorMessage}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{!startupConfigError && !isFetching && (
|
||||
<>
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Registration form"
|
||||
|
|
@ -251,7 +138,8 @@ const Registration: React.FC = () => {
|
|||
},
|
||||
})}
|
||||
{renderInput('confirm_password', 'com_auth_password_confirm', 'password', {
|
||||
validate: (value) => value === password || localize('com_auth_password_not_match'),
|
||||
validate: (value: string) =>
|
||||
value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
<div className="mt-6">
|
||||
<button
|
||||
|
|
@ -264,39 +152,16 @@ const Registration: React.FC = () => {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
||||
{localize('com_auth_already_have_account')}{' '}
|
||||
<a href="/login" aria-label="Login" className="p-1 text-green-500">
|
||||
{localize('com_auth_login')}
|
||||
</a>
|
||||
</p>
|
||||
{startupConfig.socialLoginEnabled && (
|
||||
<>
|
||||
{startupConfig.emailLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t border-gray-300 uppercase dark:border-gray-600">
|
||||
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
|
||||
Or
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8" />
|
||||
</>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
{socialLogins.map((provider) => providerComponents[provider] || null)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="align-end m-4 flex justify-center gap-2">
|
||||
{privacyPolicyRender}
|
||||
{privacyPolicyRender && termsOfServiceRender && (
|
||||
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
{termsOfServiceRender}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { useForm } from 'react-hook-form';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
useGetStartupConfig,
|
||||
useRequestPasswordResetMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useRequestPasswordResetMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TRequestPasswordReset, TRequestPasswordResetResponse } from 'librechat-data-provider';
|
||||
import { ThemeSelector } from '~/components/ui';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
function RequestPasswordReset() {
|
||||
|
|
@ -15,187 +13,131 @@ function RequestPasswordReset() {
|
|||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<TRequestPasswordReset>();
|
||||
const requestPasswordReset = useRequestPasswordResetMutation();
|
||||
const config = useGetStartupConfig();
|
||||
const [requestError, setRequestError] = useState<boolean>(false);
|
||||
const [resetLink, setResetLink] = useState<string | undefined>(undefined);
|
||||
const [headerText, setHeaderText] = useState<string>('');
|
||||
const [bodyText, setBodyText] = useState<React.ReactNode | undefined>(undefined);
|
||||
const { startupConfig, setError, setHeaderText } = useOutletContext<TLoginLayoutContext>();
|
||||
|
||||
const requestPasswordReset = useRequestPasswordResetMutation();
|
||||
|
||||
const onSubmit = (data: TRequestPasswordReset) => {
|
||||
requestPasswordReset.mutate(data, {
|
||||
onSuccess: (data: TRequestPasswordResetResponse) => {
|
||||
console.log('emailEnabled: ', config.data?.emailEnabled);
|
||||
if (!config.data?.emailEnabled) {
|
||||
if (!startupConfig?.emailEnabled) {
|
||||
setResetLink(data.link);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setRequestError(true);
|
||||
setError('com_auth_error_reset_password');
|
||||
setTimeout(() => {
|
||||
setRequestError(false);
|
||||
setError(null);
|
||||
}, 5000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (requestPasswordReset.isSuccess) {
|
||||
if (config.data?.emailEnabled) {
|
||||
setHeaderText(localize('com_auth_reset_password_link_sent'));
|
||||
setBodyText(localize('com_auth_reset_password_email_sent'));
|
||||
} else {
|
||||
setHeaderText(localize('com_auth_reset_password'));
|
||||
setBodyText(
|
||||
<span>
|
||||
{localize('com_auth_click')}{' '}
|
||||
<a className="text-green-500 hover:underline" href={resetLink}>
|
||||
{localize('com_auth_here')}
|
||||
</a>{' '}
|
||||
{localize('com_auth_to_reset_your_password')}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setHeaderText(localize('com_auth_reset_password'));
|
||||
if (!requestPasswordReset.isSuccess) {
|
||||
setHeaderText('com_auth_reset_password');
|
||||
setBodyText(undefined);
|
||||
return;
|
||||
}
|
||||
}, [requestPasswordReset.isSuccess, config.data?.emailEnabled, resetLink, localize]);
|
||||
|
||||
const renderFormContent = () => {
|
||||
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>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Password reset form"
|
||||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
autoComplete="off"
|
||||
aria-label={localize('com_auth_email')}
|
||||
{...register('email', {
|
||||
required: localize('com_auth_email_required'),
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: localize('com_auth_email_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: localize('com_auth_email_max_length'),
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: localize('com_auth_email_pattern'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
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')}
|
||||
</label>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.email.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!errors.email}
|
||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500"
|
||||
>
|
||||
{localize('com_auth_continue')}
|
||||
</button>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<a href="/login" className="text-sm text-green-500">
|
||||
{localize('com_auth_back_to_login')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
if (startupConfig?.emailEnabled) {
|
||||
setHeaderText('com_auth_reset_password_link_sent');
|
||||
setBodyText(localize('com_auth_reset_password_email_sent'));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const privacyPolicy = config.data?.interface?.privacyPolicy;
|
||||
const termsOfService = config.data?.interface?.termsOfService;
|
||||
setHeaderText('com_auth_reset_password');
|
||||
setBodyText(
|
||||
<span>
|
||||
{localize('com_auth_click')}{' '}
|
||||
<a className="text-green-500 hover:underline" href={resetLink}>
|
||||
{localize('com_auth_here')}
|
||||
</a>{' '}
|
||||
{localize('com_auth_to_reset_your_password')}
|
||||
</span>,
|
||||
);
|
||||
}, [
|
||||
requestPasswordReset.isSuccess,
|
||||
startupConfig?.emailEnabled,
|
||||
resetLink,
|
||||
localize,
|
||||
setHeaderText,
|
||||
]);
|
||||
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={privacyPolicy.externalUrl}
|
||||
target={privacyPolicy.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</a>
|
||||
);
|
||||
|
||||
const termsOfServiceRender = termsOfService?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={termsOfService.externalUrl}
|
||||
target={termsOfService.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
</a>
|
||||
);
|
||||
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 (
|
||||
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
|
||||
<div className="mt-12 h-24 w-full bg-cover">
|
||||
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Password reset form"
|
||||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
autoComplete="off"
|
||||
aria-label={localize('com_auth_email')}
|
||||
{...register('email', {
|
||||
required: localize('com_auth_email_required'),
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: localize('com_auth_email_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: localize('com_auth_email_max_length'),
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: localize('com_auth_email_pattern'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
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')}
|
||||
</label>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.email.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 md:m-4">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center">
|
||||
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white">
|
||||
{headerText}
|
||||
</h1>
|
||||
{requestError && (
|
||||
<div
|
||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_auth_error_reset_password')}
|
||||
</div>
|
||||
)}
|
||||
{renderFormContent()}
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!errors.email}
|
||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500"
|
||||
>
|
||||
{localize('com_auth_continue')}
|
||||
</button>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<a href="/login" className="text-sm text-green-500">
|
||||
{localize('com_auth_back_to_login')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="align-end m-4 flex justify-center gap-2">
|
||||
{privacyPolicyRender}
|
||||
{privacyPolicyRender && termsOfServiceRender && (
|
||||
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
{termsOfServiceRender}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useGetStartupConfig, useResetPasswordMutation } from 'librechat-data-provider/react-query';
|
||||
import { useResetPasswordMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TResetPassword } from 'librechat-data-provider';
|
||||
import { ThemeSelector } from '~/components/ui';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
function ResetPassword() {
|
||||
|
|
@ -14,218 +14,146 @@ function ResetPassword() {
|
|||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<TResetPassword>();
|
||||
const resetPassword = useResetPasswordMutation();
|
||||
const config = useGetStartupConfig();
|
||||
const [resetError, setResetError] = useState<boolean>(false);
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
const password = watch('password');
|
||||
const resetPassword = useResetPasswordMutation();
|
||||
const { setError, setHeaderText } = useOutletContext<TLoginLayoutContext>();
|
||||
|
||||
const onSubmit = (data: TResetPassword) => {
|
||||
resetPassword.mutate(data, {
|
||||
onError: () => {
|
||||
setResetError(true);
|
||||
setError('com_auth_error_invalid_reset_token');
|
||||
},
|
||||
onSuccess: () => {
|
||||
setHeaderText('com_auth_reset_password_success');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const privacyPolicy = config.data?.interface?.privacyPolicy;
|
||||
const termsOfService = config.data?.interface?.termsOfService;
|
||||
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={privacyPolicy.externalUrl}
|
||||
target={privacyPolicy.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</a>
|
||||
);
|
||||
|
||||
const termsOfServiceRender = termsOfService?.externalUrl && (
|
||||
<a
|
||||
className="text-sm text-green-500"
|
||||
href={termsOfService.externalUrl}
|
||||
target={termsOfService.openNewTab ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
</a>
|
||||
);
|
||||
|
||||
if (resetPassword.isSuccess) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0">
|
||||
<div className="absolute bottom-0 left-0 m-4">
|
||||
<ThemeSelector />
|
||||
<>
|
||||
<div
|
||||
className="relative mb-8 mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700 dark:bg-gray-900 dark:text-white"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_auth_login_with_new_password')}
|
||||
</div>
|
||||
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white">
|
||||
{localize('com_auth_reset_password_success')}
|
||||
</h1>
|
||||
<div
|
||||
className="relative mb-8 mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700 dark:bg-gray-900 dark:text-white"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_auth_login_with_new_password')}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
aria-label={localize('com_auth_sign_in')}
|
||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||
>
|
||||
{localize('com_auth_continue')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
|
||||
<div className="mt-12 h-24 w-full bg-cover">
|
||||
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 md:m-4">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center">
|
||||
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white">
|
||||
{localize('com_auth_reset_password')}
|
||||
</h1>
|
||||
{resetError && (
|
||||
<div
|
||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_auth_error_invalid_reset_token')}{' '}
|
||||
<a className="font-semibold text-green-600 hover:underline" href="/forgot-password">
|
||||
{localize('com_auth_click_here')}
|
||||
</a>{' '}
|
||||
{localize('com_auth_to_try_again')}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Password reset form"
|
||||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="hidden"
|
||||
id="token"
|
||||
// @ts-ignore - Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'
|
||||
value={params.get('token')}
|
||||
{...register('token', { required: 'Unable to process: No valid reset token' })}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="userId"
|
||||
// @ts-ignore - Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'
|
||||
value={params.get('userId')}
|
||||
{...register('userId', { required: 'Unable to process: No valid user id' })}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label={localize('com_auth_password')}
|
||||
{...register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: localize('com_auth_password_max_length'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="password"
|
||||
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_password')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{errors.password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.password.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
aria-label={localize('com_auth_password_confirm')}
|
||||
{...register('confirm_password', {
|
||||
validate: (value) =>
|
||||
value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="confirm_password"
|
||||
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_password_confirm')}
|
||||
</label>
|
||||
</div>
|
||||
{errors.confirm_password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.confirm_password.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.token && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.token.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.userId.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
disabled={!!errors.password || !!errors.confirm_password}
|
||||
type="submit"
|
||||
aria-label={localize('com_auth_submit_registration')}
|
||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-all duration-300 hover:bg-green-550 focus:bg-green-550 focus:outline-none"
|
||||
>
|
||||
{localize('com_auth_continue')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="align-end m-4 flex justify-center gap-2">
|
||||
{privacyPolicyRender}
|
||||
{privacyPolicyRender && termsOfServiceRender && (
|
||||
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
{termsOfServiceRender}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
aria-label={localize('com_auth_sign_in')}
|
||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||
>
|
||||
{localize('com_auth_continue')}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Password reset form"
|
||||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="hidden"
|
||||
id="token"
|
||||
value={params.get('token') ?? ''}
|
||||
{...register('token', { required: 'Unable to process: No valid reset token' })}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="userId"
|
||||
value={params.get('userId') ?? ''}
|
||||
{...register('userId', { required: 'Unable to process: No valid user id' })}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label={localize('com_auth_password')}
|
||||
{...register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: localize('com_auth_password_max_length'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
htmlFor="password"
|
||||
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_password')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{errors.password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.password.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
aria-label={localize('com_auth_password_confirm')}
|
||||
{...register('confirm_password', {
|
||||
validate: (value) => value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
htmlFor="confirm_password"
|
||||
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_password_confirm')}
|
||||
</label>
|
||||
</div>
|
||||
{errors.confirm_password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.confirm_password.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.token && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.token.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.userId.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
disabled={!!errors.password || !!errors.confirm_password}
|
||||
type="submit"
|
||||
aria-label={localize('com_auth_submit_registration')}
|
||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-all duration-300 hover:bg-green-550 focus:bg-green-550 focus:outline-none"
|
||||
>
|
||||
{localize('com_auth_continue')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetPassword;
|
||||
|
|
|
|||
105
client/src/components/Auth/SocialLoginRender.tsx
Normal file
105
client/src/components/Auth/SocialLoginRender.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
||||
|
||||
import SocialButton from './SocialButton';
|
||||
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
import { TStartupConfig } from 'librechat-data-provider';
|
||||
|
||||
function SocialLoginRender({
|
||||
startupConfig,
|
||||
}: {
|
||||
startupConfig: TStartupConfig | null | undefined;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
if (!startupConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerComponents = {
|
||||
discord: startupConfig?.discordLoginEnabled && (
|
||||
<SocialButton
|
||||
key="discord"
|
||||
enabled={startupConfig.discordLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="discord"
|
||||
Icon={DiscordIcon}
|
||||
label={localize('com_auth_discord_login')}
|
||||
id="discord"
|
||||
/>
|
||||
),
|
||||
facebook: startupConfig?.facebookLoginEnabled && (
|
||||
<SocialButton
|
||||
key="facebook"
|
||||
enabled={startupConfig.facebookLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="facebook"
|
||||
Icon={FacebookIcon}
|
||||
label={localize('com_auth_facebook_login')}
|
||||
id="facebook"
|
||||
/>
|
||||
),
|
||||
github: startupConfig?.githubLoginEnabled && (
|
||||
<SocialButton
|
||||
key="github"
|
||||
enabled={startupConfig.githubLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="github"
|
||||
Icon={GithubIcon}
|
||||
label={localize('com_auth_github_login')}
|
||||
id="github"
|
||||
/>
|
||||
),
|
||||
google: startupConfig?.googleLoginEnabled && (
|
||||
<SocialButton
|
||||
key="google"
|
||||
enabled={startupConfig.googleLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="google"
|
||||
Icon={GoogleIcon}
|
||||
label={localize('com_auth_google_login')}
|
||||
id="google"
|
||||
/>
|
||||
),
|
||||
openid: startupConfig?.openidLoginEnabled && (
|
||||
<SocialButton
|
||||
key="openid"
|
||||
enabled={startupConfig.openidLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="openid"
|
||||
Icon={() =>
|
||||
startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)
|
||||
}
|
||||
label={startupConfig.openidLabel}
|
||||
id="openid"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
startupConfig.socialLoginEnabled && (
|
||||
<>
|
||||
{startupConfig.emailLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t border-gray-300 uppercase dark:border-gray-600">
|
||||
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
|
||||
Or
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8" />
|
||||
</>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
{startupConfig.socialLogins?.map((provider) => providerComponents[provider] || null)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default SocialLoginRender;
|
||||
|
|
@ -1,10 +1,33 @@
|
|||
import { render, waitFor } from 'test/layout-test-utils';
|
||||
import reactRouter from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Login from '../Login';
|
||||
import { render, waitFor } from 'test/layout-test-utils';
|
||||
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import AuthLayout from '~/components/Auth/AuthLayout';
|
||||
import Login from '~/components/Auth/Login';
|
||||
|
||||
jest.mock('librechat-data-provider/react-query');
|
||||
|
||||
const mockStartupConfig = {
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
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,
|
||||
serverDomain: 'mock-server',
|
||||
},
|
||||
};
|
||||
|
||||
const setup = ({
|
||||
useGetUserQueryReturnValue = {
|
||||
isLoading: false,
|
||||
|
|
@ -27,24 +50,7 @@ const setup = ({
|
|||
user: {},
|
||||
},
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
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,
|
||||
serverDomain: 'mock-server',
|
||||
},
|
||||
},
|
||||
useGetStartupCongfigReturnValue = mockStartupConfig,
|
||||
} = {}) => {
|
||||
const mockUseLoginUser = jest
|
||||
.spyOn(mockDataProvider, 'useLoginUserMutation')
|
||||
|
|
@ -62,16 +68,38 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useRefreshTokenMutationReturnValue);
|
||||
const renderResult = render(<Login />);
|
||||
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({
|
||||
startupConfig: useGetStartupCongfigReturnValue.data,
|
||||
});
|
||||
const renderResult = render(
|
||||
<AuthLayout
|
||||
startupConfig={useGetStartupCongfigReturnValue.data as TStartupConfig}
|
||||
isFetching={useGetStartupCongfigReturnValue.isFetching}
|
||||
error={null}
|
||||
startupConfigError={null}
|
||||
header={'Welcome back'}
|
||||
pathname="login"
|
||||
>
|
||||
<Login />
|
||||
</AuthLayout>,
|
||||
);
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseLoginUser,
|
||||
mockUseGetUserQuery,
|
||||
mockUseOutletContext,
|
||||
mockUseGetStartupConfig,
|
||||
mockUseRefreshTokenMutation,
|
||||
};
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useOutletContext: () => ({
|
||||
startupConfig: mockStartupConfig,
|
||||
}),
|
||||
}));
|
||||
|
||||
test('renders login form', () => {
|
||||
const { getByLabelText, getByRole } = setup();
|
||||
expect(getByLabelText(/email/i)).toBeInTheDocument();
|
||||
|
|
@ -132,6 +160,14 @@ test('Navigates to / on successful login', async () => {
|
|||
isError: false,
|
||||
isSuccess: true,
|
||||
},
|
||||
useGetStartupCongfigReturnValue: {
|
||||
...mockStartupConfig,
|
||||
data: {
|
||||
...mockStartupConfig.data,
|
||||
emailLoginEnabled: true,
|
||||
registrationEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emailInput = getByLabelText(/email/i);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,32 @@
|
|||
import { render, waitFor, screen } from 'test/layout-test-utils';
|
||||
import reactRouter from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Registration from '../Registration';
|
||||
import { render, waitFor, screen } from 'test/layout-test-utils';
|
||||
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import Registration from '~/components/Auth/Registration';
|
||||
import AuthLayout from '~/components/Auth/AuthLayout';
|
||||
|
||||
jest.mock('librechat-data-provider/react-query');
|
||||
|
||||
const mockStartupConfig = {
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
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,
|
||||
socialLoginEnabled: true,
|
||||
serverDomain: 'mock-server',
|
||||
},
|
||||
};
|
||||
|
||||
const setup = ({
|
||||
useGetUserQueryReturnValue = {
|
||||
isLoading: false,
|
||||
|
|
@ -28,23 +50,7 @@ const setup = ({
|
|||
user: {},
|
||||
},
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
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,
|
||||
socialLoginEnabled: true,
|
||||
serverDomain: 'mock-server',
|
||||
},
|
||||
},
|
||||
useGetStartupCongfigReturnValue = mockStartupConfig,
|
||||
} = {}) => {
|
||||
const mockUseRegisterUserMutation = jest
|
||||
.spyOn(mockDataProvider, 'useRegisterUserMutation')
|
||||
|
|
@ -62,17 +68,39 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useRefreshTokenMutationReturnValue);
|
||||
const renderResult = render(<Registration />);
|
||||
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({
|
||||
startupConfig: useGetStartupCongfigReturnValue.data,
|
||||
});
|
||||
const renderResult = render(
|
||||
<AuthLayout
|
||||
startupConfig={useGetStartupCongfigReturnValue.data as TStartupConfig}
|
||||
isFetching={useGetStartupCongfigReturnValue.isFetching}
|
||||
error={null}
|
||||
startupConfigError={null}
|
||||
header={'Create your account'}
|
||||
pathname="register"
|
||||
>
|
||||
<Registration />
|
||||
</AuthLayout>,
|
||||
);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseRegisterUserMutation,
|
||||
mockUseGetUserQuery,
|
||||
mockUseOutletContext,
|
||||
mockUseGetStartupConfig,
|
||||
mockUseRegisterUserMutation,
|
||||
mockUseRefreshTokenMutation,
|
||||
};
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useOutletContext: () => ({
|
||||
startupConfig: mockStartupConfig,
|
||||
}),
|
||||
}));
|
||||
|
||||
test('renders registration form', () => {
|
||||
const { getByText, getByTestId, getByRole } = setup();
|
||||
expect(getByText(/Create your account/i)).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import { getConfigDefaults } from 'librechat-data-provider';
|
|||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type { ContextType } from '~/common';
|
||||
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||
import HeaderOptions from './Input/HeaderOptions';
|
||||
import ExportButton from './ExportButton';
|
||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||
import HeaderOptions from './Input/HeaderOptions';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Import } from 'lucide-react';
|
||||
import type { TError } from 'librechat-data-provider';
|
||||
import { useUploadConversationsMutation } from '~/data-provider';
|
||||
import { useLocalize, useConversations } from '~/hooks';
|
||||
import { useToastContext } from '~/Providers';
|
||||
|
|
@ -25,8 +26,7 @@ function ImportConversations() {
|
|||
console.error('Error: ', error);
|
||||
setAllowImport(true);
|
||||
setError(
|
||||
(error as { response: { data: { message?: string } } })?.response?.data?.message ??
|
||||
'An error occurred while uploading the file.',
|
||||
(error as TError)?.response?.data?.message ?? 'An error occurred while uploading the file.',
|
||||
);
|
||||
if (error?.toString().includes('Unsupported import type')) {
|
||||
showToast({
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const AuthContextProvider = ({
|
|||
loginUser.mutate(data, {
|
||||
onSuccess: (data: TLoginResponse) => {
|
||||
const { user, token } = data;
|
||||
setError(undefined);
|
||||
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
|
||||
},
|
||||
onError: (error: TResError | unknown) => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
defaultAssistantsVersion,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig, TError } from 'librechat-data-provider';
|
||||
import type { ExtendedFile, FileSetter } from '~/common';
|
||||
import { useUploadFileMutation, useGetFileConfig } from '~/data-provider';
|
||||
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
||||
|
|
@ -118,8 +118,7 @@ const useFileHandling = (params?: UseFileHandling) => {
|
|||
clearUploadTimer(file_id as string);
|
||||
deleteFileById(file_id as string);
|
||||
setError(
|
||||
(error as { response: { data: { message?: string } } })?.response?.data?.message ??
|
||||
'An error occurred while uploading the file.',
|
||||
(error as TError)?.response?.data?.message ?? 'An error occurred while uploading the file.',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
7
client/src/routes/Layouts/Login.tsx
Normal file
7
client/src/routes/Layouts/Login.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import StartupLayout from './Startup';
|
||||
|
||||
export default function LoginLayout() {
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
return <StartupLayout isAuthenticated={isAuthenticated} />;
|
||||
}
|
||||
70
client/src/routes/Layouts/Startup.tsx
Normal file
70
client/src/routes/Layouts/Startup.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import AuthLayout from '~/components/Auth/AuthLayout';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const headerMap = {
|
||||
'/login': 'com_auth_welcome_back',
|
||||
'/register': 'com_auth_create_account',
|
||||
'/forgot-password': 'com_auth_reset_password',
|
||||
'/reset-password': 'com_auth_reset_password',
|
||||
};
|
||||
|
||||
export default function StartupLayout({ isAuthenticated }: { isAuthenticated?: boolean }) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [headerText, setHeaderText] = useState<string | null>(null);
|
||||
const [startupConfig, setStartupConfig] = useState<TStartupConfig | null>(null);
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
error: startupConfigError,
|
||||
} = useGetStartupConfig({
|
||||
enabled: isAuthenticated ? startupConfig === null : true,
|
||||
});
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
if (data) {
|
||||
setStartupConfig(data);
|
||||
}
|
||||
}, [isAuthenticated, navigate, data]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = startupConfig?.appTitle || 'LibreChat';
|
||||
}, [startupConfig?.appTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
setError(null);
|
||||
setHeaderText(null);
|
||||
}, [location.pathname]);
|
||||
|
||||
const contextValue = {
|
||||
error,
|
||||
setError,
|
||||
headerText,
|
||||
setHeaderText,
|
||||
startupConfigError,
|
||||
startupConfig,
|
||||
isFetching,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout
|
||||
header={headerText ? localize(headerText) : localize(headerMap[location.pathname])}
|
||||
isFetching={isFetching}
|
||||
startupConfig={startupConfig}
|
||||
startupConfigError={startupConfigError}
|
||||
pathname={location.pathname}
|
||||
error={error}
|
||||
>
|
||||
<Outlet context={contextValue} />
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import {
|
|||
ApiErrorWatcher,
|
||||
} from '~/components/Auth';
|
||||
import { AuthContextProvider } from '~/hooks/AuthContext';
|
||||
import StartupLayout from './Layouts/Startup';
|
||||
import LoginLayout from './Layouts/Login';
|
||||
import ShareRoute from './ShareRoute';
|
||||
import ChatRoute from './ChatRoute';
|
||||
import Search from './Search';
|
||||
|
|
@ -20,28 +22,40 @@ const AuthLayout = () => (
|
|||
);
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: 'register',
|
||||
element: <Registration />,
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
element: <RequestPasswordReset />,
|
||||
},
|
||||
{
|
||||
path: 'reset-password',
|
||||
element: <ResetPassword />,
|
||||
},
|
||||
{
|
||||
path: 'share/:shareId',
|
||||
element: <ShareRoute />,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <StartupLayout />,
|
||||
children: [
|
||||
{
|
||||
path: 'register',
|
||||
element: <Registration />,
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
element: <RequestPasswordReset />,
|
||||
},
|
||||
{
|
||||
path: 'reset-password',
|
||||
element: <ResetPassword />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{
|
||||
path: 'login',
|
||||
element: <Login />,
|
||||
path: '/',
|
||||
element: <LoginLayout />,
|
||||
children: [
|
||||
{
|
||||
path: 'login',
|
||||
element: <Login />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue