mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🔄🔐 refactor: auth; style: match OpenAI; feat: custom social login order (#1421)
* refactor(Login & Registration) * fix(Registration) test errors * refactor(LoginForm & ResetPassword) * fix(LoginForm): display 'undefined' when loading page; style(SocialButton): match OpenAI's graphics * some refactor and style update for social logins * style: width like OpenAI; feat: custom social login order; refactor: alphabetical socials * fix(Registration & Login) test * Update .env.example * Update .env.example * Update dotenv.md * refactor: remove `SOCIAL_LOGIN_ORDER` for `socialLogins` configured from `librechat.yaml` - initialized by AppService, attached as app.locals property - rename socialLoginOrder and loginOrder to socialLogins app-wide for consistency - update types and docs - initialize config variable as array and not singular string to parse - bump data-provider to 0.3.9 --------- Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
This commit is contained in:
parent
25da90657d
commit
a2c35e8415
21 changed files with 536 additions and 532 deletions
|
|
@ -56,13 +56,14 @@ describe.skip('GET /', () => {
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
appTitle: 'Test Title',
|
appTitle: 'Test Title',
|
||||||
googleLoginEnabled: true,
|
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
|
||||||
|
discordLoginEnabled: true,
|
||||||
facebookLoginEnabled: true,
|
facebookLoginEnabled: true,
|
||||||
|
githubLoginEnabled: true,
|
||||||
|
googleLoginEnabled: true,
|
||||||
openidLoginEnabled: true,
|
openidLoginEnabled: true,
|
||||||
openidLabel: 'Test OpenID',
|
openidLabel: 'Test OpenID',
|
||||||
openidImageUrl: 'http://test-server.com',
|
openidImageUrl: 'http://test-server.com',
|
||||||
githubLoginEnabled: true,
|
|
||||||
discordLoginEnabled: true,
|
|
||||||
serverDomain: 'http://test-server.com',
|
serverDomain: 'http://test-server.com',
|
||||||
emailLoginEnabled: 'true',
|
emailLoginEnabled: 'true',
|
||||||
registrationEnabled: 'true',
|
registrationEnabled: 'true',
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,19 @@ router.get('/', async function (req, res) {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
appTitle: process.env.APP_TITLE || 'LibreChat',
|
appTitle: process.env.APP_TITLE || 'LibreChat',
|
||||||
googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
|
socialLogins: req.app.locals.socialLogins,
|
||||||
|
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
|
||||||
facebookLoginEnabled:
|
facebookLoginEnabled:
|
||||||
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
|
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
|
||||||
|
githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
|
||||||
openidLoginEnabled:
|
openidLoginEnabled:
|
||||||
!!process.env.OPENID_CLIENT_ID &&
|
!!process.env.OPENID_CLIENT_ID &&
|
||||||
!!process.env.OPENID_CLIENT_SECRET &&
|
!!process.env.OPENID_CLIENT_SECRET &&
|
||||||
!!process.env.OPENID_ISSUER &&
|
!!process.env.OPENID_ISSUER &&
|
||||||
!!process.env.OPENID_SESSION_SECRET,
|
!!process.env.OPENID_SESSION_SECRET,
|
||||||
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Login with OpenID',
|
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
|
||||||
openidImageUrl: process.env.OPENID_IMAGE_URL,
|
openidImageUrl: process.env.OPENID_IMAGE_URL,
|
||||||
githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
|
|
||||||
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
|
|
||||||
serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
|
serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
|
||||||
emailLoginEnabled,
|
emailLoginEnabled,
|
||||||
registrationEnabled: isEnabled(process.env.ALLOW_REGISTRATION),
|
registrationEnabled: isEnabled(process.env.ALLOW_REGISTRATION),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,15 @@ const paths = require('~/config/paths');
|
||||||
* @param {Express.Application} app - The Express application object.
|
* @param {Express.Application} app - The Express application object.
|
||||||
*/
|
*/
|
||||||
const AppService = async (app) => {
|
const AppService = async (app) => {
|
||||||
|
/** @type {TCustomConfig}*/
|
||||||
const config = (await loadCustomConfig()) ?? {};
|
const config = (await loadCustomConfig()) ?? {};
|
||||||
|
const socialLogins = config.registration.socialLogins ?? [
|
||||||
|
'google',
|
||||||
|
'facebook',
|
||||||
|
'openid',
|
||||||
|
'github',
|
||||||
|
'discord',
|
||||||
|
];
|
||||||
const fileStrategy = config.fileStrategy ?? FileSources.local;
|
const fileStrategy = config.fileStrategy ?? FileSources.local;
|
||||||
process.env.CDN_PROVIDER = fileStrategy;
|
process.env.CDN_PROVIDER = fileStrategy;
|
||||||
|
|
||||||
|
|
@ -19,6 +27,7 @@ const AppService = async (app) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
app.locals = {
|
app.locals = {
|
||||||
|
socialLogins,
|
||||||
fileStrategy,
|
fileStrategy,
|
||||||
paths,
|
paths,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const configPath = path.resolve(projectRoot, 'librechat.yaml');
|
||||||
* Load custom configuration files and caches the object if the `cache` field at root is true.
|
* Load custom configuration files and caches the object if the `cache` field at root is true.
|
||||||
* Validation via parsing the config file with the config schema.
|
* Validation via parsing the config file with the config schema.
|
||||||
* @function loadCustomConfig
|
* @function loadCustomConfig
|
||||||
* @returns {Promise<null | Object>} A promise that resolves to null or the custom config object.
|
* @returns {Promise<TCustomConfig | null>} A promise that resolves to null or the custom config object.
|
||||||
* */
|
* */
|
||||||
|
|
||||||
async function loadCustomConfig() {
|
async function loadCustomConfig() {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { getLoginError } from '~/utils';
|
import { getLoginError } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import LoginForm from './LoginForm';
|
import LoginForm from './LoginForm';
|
||||||
|
import SocialButton from './SocialButton';
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const { login, error, isAuthenticated } = useAuthContext();
|
const { login, error, isAuthenticated } = useAuthContext();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -20,9 +20,79 @@ function Login() {
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate]);
|
}, [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"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||||
{localize('com_auth_welcome_back')}
|
{localize('com_auth_welcome_back')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -34,95 +104,28 @@ function Login() {
|
||||||
{localize(getLoginError(error))}
|
{localize(getLoginError(error))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{startupConfig?.emailLoginEnabled && <LoginForm onSubmit={login} />}
|
{startupConfig.emailLoginEnabled && <LoginForm onSubmit={login} />}
|
||||||
{startupConfig?.registrationEnabled && (
|
{startupConfig.registrationEnabled && (
|
||||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||||
{' '}
|
{' '}
|
||||||
{localize('com_auth_no_account')}{' '}
|
{localize('com_auth_no_account')}{' '}
|
||||||
<a href="/register" className="p-1 font-medium text-green-500 hover:underline">
|
<a href="/register" className="p-1 font-medium text-green-500">
|
||||||
{localize('com_auth_sign_up')}
|
{localize('com_auth_sign_up')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{startupConfig?.socialLoginEnabled && startupConfig?.emailLoginEnabled && (
|
{startupConfig.socialLoginEnabled && (
|
||||||
<>
|
<>
|
||||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
{startupConfig.emailLoginEnabled && (
|
||||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
<>
|
||||||
</div>
|
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||||
<div className="mt-8" />
|
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
<div className="mt-8" />
|
||||||
{startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && (
|
</>
|
||||||
<>
|
)}
|
||||||
<div className="mt-2 flex gap-x-2">
|
<div className="mt-2">
|
||||||
<a
|
{socialLogins.map((provider) => providerComponents[provider] || null)}
|
||||||
aria-label="Login with Google"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/google`}
|
|
||||||
>
|
|
||||||
<GoogleIcon />
|
|
||||||
<p>{localize('com_auth_google_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with Facebook"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/facebook`}
|
|
||||||
>
|
|
||||||
<FacebookIcon />
|
|
||||||
<p>{localize('com_auth_facebook_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with OpenID"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/openid`}
|
|
||||||
>
|
|
||||||
{startupConfig.openidImageUrl ? (
|
|
||||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<OpenIDIcon />
|
|
||||||
)}
|
|
||||||
<p>{startupConfig.openidLabel}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with GitHub"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/github`}
|
|
||||||
>
|
|
||||||
<GithubIcon />
|
|
||||||
<p>{localize('com_auth_github_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with Discord"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/discord`}
|
|
||||||
>
|
|
||||||
<DiscordIcon />
|
|
||||||
<p>{localize('com_auth_discord_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { TLoginUser } from 'librechat-data-provider';
|
import { TLoginUser } from 'librechat-data-provider';
|
||||||
|
|
@ -6,15 +7,23 @@ type TLoginFormProps = {
|
||||||
onSubmit: (data: TLoginUser) => void;
|
onSubmit: (data: TLoginUser) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function LoginForm({ onSubmit }: TLoginFormProps) {
|
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TLoginUser>();
|
} = useForm<TLoginUser>();
|
||||||
|
|
||||||
|
const renderError = (fieldName: string) => {
|
||||||
|
const errorMessage = errors[fieldName]?.message;
|
||||||
|
return errorMessage ? (
|
||||||
|
<span role="alert" className="mt-1 text-sm text-black">
|
||||||
|
{String(errorMessage)}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
|
|
@ -31,23 +40,14 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
|
||||||
aria-label={localize('com_auth_email')}
|
aria-label={localize('com_auth_email')}
|
||||||
{...register('email', {
|
{...register('email', {
|
||||||
required: localize('com_auth_email_required'),
|
required: localize('com_auth_email_required'),
|
||||||
minLength: {
|
minLength: { value: 3, message: localize('com_auth_email_min_length') },
|
||||||
value: 3,
|
maxLength: { value: 120, message: localize('com_auth_email_max_length') },
|
||||||
message: localize('com_auth_email_min_length'),
|
pattern: { value: /\S+@\S+\.\S+/, message: localize('com_auth_email_pattern') },
|
||||||
},
|
|
||||||
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}
|
aria-invalid={!!errors.email}
|
||||||
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
></input>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||||
|
|
@ -55,12 +55,7 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
|
||||||
{localize('com_auth_email_address')}
|
{localize('com_auth_email_address')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{errors.email && (
|
{renderError('email')}
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @ts-ignore not sure why*/}
|
|
||||||
{errors.email.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -71,19 +66,13 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
|
||||||
aria-label={localize('com_auth_password')}
|
aria-label={localize('com_auth_password')}
|
||||||
{...register('password', {
|
{...register('password', {
|
||||||
required: localize('com_auth_password_required'),
|
required: localize('com_auth_password_required'),
|
||||||
minLength: {
|
minLength: { value: 8, message: localize('com_auth_password_min_length') },
|
||||||
value: 8,
|
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
||||||
message: localize('com_auth_password_min_length'),
|
|
||||||
},
|
|
||||||
maxLength: {
|
|
||||||
value: 128,
|
|
||||||
message: localize('com_auth_password_max_length'),
|
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.password}
|
aria-invalid={!!errors.password}
|
||||||
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
></input>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||||
|
|
@ -91,15 +80,9 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
|
||||||
{localize('com_auth_password')}
|
{localize('com_auth_password')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{renderError('password')}
|
||||||
{errors.password && (
|
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @ts-ignore not sure why*/}
|
|
||||||
{errors.password.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<a href="/forgot-password" className="text-sm font-medium text-green-500 hover:underline">
|
<a href="/forgot-password" className="text-sm font-medium text-green-500">
|
||||||
{localize('com_auth_password_forgot')}
|
{localize('com_auth_password_forgot')}
|
||||||
</a>
|
</a>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|
@ -107,12 +90,13 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
|
||||||
aria-label="Sign in"
|
aria-label="Sign in"
|
||||||
data-testid="login-button"
|
data-testid="login-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
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">
|
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')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LoginForm;
|
export default LoginForm;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useRegisterUserMutation, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
import { useRegisterUserMutation, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import type { TRegisterUser } from 'librechat-data-provider';
|
import type { TRegisterUser } from 'librechat-data-provider';
|
||||||
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import SocialButton from './SocialButton';
|
||||||
|
|
||||||
function Registration() {
|
const Registration: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -22,23 +22,20 @@ function Registration() {
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
const registerUser = useRegisterUserMutation();
|
const registerUser = useRegisterUserMutation();
|
||||||
|
|
||||||
const password = watch('password');
|
const password = watch('password');
|
||||||
|
|
||||||
const onRegisterUserFormSubmit = (data: TRegisterUser) => {
|
const onRegisterUserFormSubmit = async (data: TRegisterUser) => {
|
||||||
registerUser.mutate(data, {
|
try {
|
||||||
onSuccess: () => {
|
await registerUser.mutateAsync(data);
|
||||||
navigate('/c/new');
|
navigate('/c/new');
|
||||||
},
|
} catch (error) {
|
||||||
onError: (error) => {
|
setError(true);
|
||||||
setError(true);
|
//@ts-ignore - error is of type unknown
|
||||||
|
if (error.response?.data?.message) {
|
||||||
//@ts-ignore - error is of type unknown
|
//@ts-ignore - error is of type unknown
|
||||||
if (error.response?.data?.message) {
|
setErrorMessage(error.response?.data?.message);
|
||||||
//@ts-ignore - error is of type unknown
|
}
|
||||||
setErrorMessage(error.response?.data?.message);
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -47,9 +44,111 @@ function Registration() {
|
||||||
}
|
}
|
||||||
}, [startupConfig, navigate]);
|
}, [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">
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
autoComplete={id}
|
||||||
|
aria-label={localize(label)}
|
||||||
|
{...register(
|
||||||
|
id as 'name' | 'email' | 'username' | 'password' | 'confirm_password',
|
||||||
|
validation,
|
||||||
|
)}
|
||||||
|
aria-invalid={!!errors[id]}
|
||||||
|
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||||
|
placeholder=" "
|
||||||
|
data-testid={id}
|
||||||
|
></input>
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||||
|
>
|
||||||
|
{localize(label)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{errors[id] && (
|
||||||
|
<span role="alert" className="mt-1 text-sm text-black">
|
||||||
|
{String(errors[id]?.message) ?? ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||||
{localize('com_auth_create_account')}
|
{localize('com_auth_create_account')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -66,204 +165,61 @@ function Registration() {
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
aria-label="Registration form"
|
aria-label="Registration form"
|
||||||
method="POST"
|
method="POST"
|
||||||
onSubmit={handleSubmit((data) => onRegisterUserFormSubmit(data))}
|
onSubmit={handleSubmit(onRegisterUserFormSubmit)}
|
||||||
>
|
>
|
||||||
<div className="mb-2">
|
{renderInput('name', 'com_auth_full_name', 'text', {
|
||||||
<div className="relative">
|
required: localize('com_auth_name_required'),
|
||||||
<input
|
minLength: {
|
||||||
id="name"
|
value: 3,
|
||||||
type="text"
|
message: localize('com_auth_name_min_length'),
|
||||||
autoComplete="name"
|
},
|
||||||
aria-label={localize('com_auth_full_name')}
|
maxLength: {
|
||||||
{...register('name', {
|
value: 80,
|
||||||
required: localize('com_auth_name_required'),
|
message: localize('com_auth_name_max_length'),
|
||||||
minLength: {
|
},
|
||||||
value: 3,
|
})}
|
||||||
message: localize('com_auth_name_min_length'),
|
{renderInput('username', 'com_auth_username', 'text', {
|
||||||
},
|
minLength: {
|
||||||
maxLength: {
|
value: 2,
|
||||||
value: 80,
|
message: localize('com_auth_username_min_length'),
|
||||||
message: localize('com_auth_name_max_length'),
|
},
|
||||||
},
|
maxLength: {
|
||||||
})}
|
value: 80,
|
||||||
aria-invalid={!!errors.name}
|
message: localize('com_auth_username_max_length'),
|
||||||
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
},
|
||||||
placeholder=" "
|
})}
|
||||||
></input>
|
{renderInput('email', 'com_auth_email', 'email', {
|
||||||
<label
|
required: localize('com_auth_email_required'),
|
||||||
htmlFor="name"
|
minLength: {
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
value: 3,
|
||||||
>
|
message: localize('com_auth_email_min_length'),
|
||||||
{localize('com_auth_full_name')}
|
},
|
||||||
</label>
|
maxLength: {
|
||||||
</div>
|
value: 120,
|
||||||
|
message: localize('com_auth_email_max_length'),
|
||||||
{errors.name && (
|
},
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
pattern: {
|
||||||
{/* @ts-ignore not sure why*/}
|
value: /\S+@\S+\.\S+/,
|
||||||
{errors.name.message}
|
message: localize('com_auth_email_pattern'),
|
||||||
</span>
|
},
|
||||||
)}
|
})}
|
||||||
</div>
|
{renderInput('password', 'com_auth_password', 'password', {
|
||||||
<div className="mb-2">
|
required: localize('com_auth_password_required'),
|
||||||
<div className="relative">
|
minLength: {
|
||||||
<input
|
value: 8,
|
||||||
type="text"
|
message: localize('com_auth_password_min_length'),
|
||||||
id="username"
|
},
|
||||||
aria-label={localize('com_auth_username')}
|
maxLength: {
|
||||||
{...register('username', {
|
value: 128,
|
||||||
// required: localize('com_auth_username_required'),
|
message: localize('com_auth_password_max_length'),
|
||||||
minLength: {
|
},
|
||||||
value: 2,
|
})}
|
||||||
message: localize('com_auth_username_min_length'),
|
{renderInput('confirm_password', 'com_auth_password_confirm', 'password', {
|
||||||
},
|
validate: (value) => value === password || localize('com_auth_password_not_match'),
|
||||||
maxLength: {
|
})}
|
||||||
value: 80,
|
|
||||||
message: localize('com_auth_username_max_length'),
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
aria-invalid={!!errors.username}
|
|
||||||
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
|
||||||
placeholder=" "
|
|
||||||
autoComplete="off"
|
|
||||||
></input>
|
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
|
||||||
>
|
|
||||||
{localize('com_auth_username')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{errors.username && (
|
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @ts-ignore not sure why */}
|
|
||||||
{errors.username.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
autoComplete="email"
|
|
||||||
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="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
|
||||||
placeholder=" "
|
|
||||||
></input>
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
|
||||||
>
|
|
||||||
{localize('com_auth_email')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */}
|
|
||||||
{errors.email.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
data-testid="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="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
|
||||||
placeholder=" "
|
|
||||||
></input>
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
|
||||||
>
|
|
||||||
{localize('com_auth_password')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{errors.password && (
|
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @ts-ignore not sure why */}
|
|
||||||
{errors.password.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="confirm_password"
|
|
||||||
data-testid="confirm_password"
|
|
||||||
aria-label={localize('com_auth_password_confirm')}
|
|
||||||
// uncomment to block pasting in confirm field
|
|
||||||
// onPaste={(e) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// return false;
|
|
||||||
// }}
|
|
||||||
{...register('confirm_password', {
|
|
||||||
validate: (value) =>
|
|
||||||
value === password || localize('com_auth_password_not_match'),
|
|
||||||
})}
|
|
||||||
aria-invalid={!!errors.confirm_password}
|
|
||||||
className="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
|
||||||
placeholder=" "
|
|
||||||
></input>
|
|
||||||
<label
|
|
||||||
htmlFor="confirm_password"
|
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
|
||||||
>
|
|
||||||
{localize('com_auth_password_confirm')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{errors.confirm_password && (
|
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @ts-ignore not sure why */}
|
|
||||||
{errors.confirm_password.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
disabled={
|
disabled={Object.keys(errors).length > 0}
|
||||||
!!errors.email ||
|
|
||||||
!!errors.name ||
|
|
||||||
!!errors.password ||
|
|
||||||
!!errors.username ||
|
|
||||||
!!errors.confirm_password
|
|
||||||
}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
aria-label="Submit registration"
|
aria-label="Submit registration"
|
||||||
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 disabled:cursor-not-allowed disabled:hover:bg-green-500"
|
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 disabled:cursor-not-allowed disabled:hover:bg-green-500"
|
||||||
|
|
@ -273,7 +229,6 @@ function Registration() {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||||
{' '}
|
|
||||||
{localize('com_auth_already_have_account')}{' '}
|
{localize('com_auth_already_have_account')}{' '}
|
||||||
<a
|
<a
|
||||||
href="/login"
|
href="/login"
|
||||||
|
|
@ -283,91 +238,24 @@ function Registration() {
|
||||||
{localize('com_auth_login')}
|
{localize('com_auth_login')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{startupConfig?.socialLoginEnabled && (
|
{startupConfig.socialLoginEnabled && (
|
||||||
<>
|
<>
|
||||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
{startupConfig.emailLoginEnabled && (
|
||||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
<>
|
||||||
</div>
|
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||||
<div className="mt-8" />
|
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
<div className="mt-8" />
|
||||||
{startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && (
|
</>
|
||||||
<>
|
)}
|
||||||
<div className="mt-2 flex gap-x-2">
|
<div className="mt-2">
|
||||||
<a
|
{socialLogins.map((provider) => providerComponents[provider] || null)}
|
||||||
aria-label="Login with Google"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/google`}
|
|
||||||
>
|
|
||||||
<GoogleIcon />
|
|
||||||
<p>{localize('com_auth_google_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with Facebook"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/facebook`}
|
|
||||||
>
|
|
||||||
<FacebookIcon />
|
|
||||||
<p>{localize('com_auth_facebook_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with OpenID"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/openid`}
|
|
||||||
>
|
|
||||||
{startupConfig.openidImageUrl ? (
|
|
||||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<OpenIDIcon />
|
|
||||||
)}
|
|
||||||
<p>{startupConfig.openidLabel}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with GitHub"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/github`}
|
|
||||||
>
|
|
||||||
<GithubIcon />
|
|
||||||
<p>{localize('com_auth_github_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mt-2 flex gap-x-2">
|
|
||||||
<a
|
|
||||||
aria-label="Login with Discord"
|
|
||||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
|
||||||
href={`${startupConfig.serverDomain}/oauth/discord`}
|
|
||||||
>
|
|
||||||
<DiscordIcon />
|
|
||||||
<p>{localize('com_auth_discord_login')}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Registration;
|
export default Registration;
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,86 @@ function RequestPasswordReset() {
|
||||||
}
|
}
|
||||||
}, [requestPasswordReset.isSuccess, config.data?.emailEnabled, resetLink, localize]);
|
}, [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"
|
||||||
|
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="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||||
|
placeholder=" "
|
||||||
|
></input>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||||
|
>
|
||||||
|
{localize('com_auth_email_address')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{errors.email && (
|
||||||
|
<span role="alert" className="mt-1 text-sm text-black">
|
||||||
|
{/* @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-all duration-300 hover:bg-green-550 focus:bg-green-550 focus:outline-none"
|
||||||
|
>
|
||||||
|
{localize('com_auth_continue')}
|
||||||
|
</button>
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<a href="/login" className="text-sm font-medium text-green-500">
|
||||||
|
{localize('com_auth_back_to_login')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
<div className="mt-5 w-authPageWidth overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||||
<h1 className="mb-4 text-center text-3xl font-semibold">{headerText}</h1>
|
<h1 className="mb-4 text-center text-3xl font-semibold">{headerText}</h1>
|
||||||
{requestError && (
|
{requestError && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -73,71 +150,7 @@ function RequestPasswordReset() {
|
||||||
{localize('com_auth_error_reset_password')}
|
{localize('com_auth_error_reset_password')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{bodyText ? (
|
{renderFormContent()}
|
||||||
<div
|
|
||||||
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
{bodyText}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<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="peer block w-full appearance-none rounded-md border border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
|
||||||
placeholder=" "
|
|
||||||
></input>
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
|
||||||
>
|
|
||||||
{localize('com_auth_email_address')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<span role="alert" className="mt-1 text-sm text-black">
|
|
||||||
{/* @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-600 focus:bg-green-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
{localize('com_auth_continue')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ function ResetPassword() {
|
||||||
if (resetPassword.isSuccess) {
|
if (resetPassword.isSuccess) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||||
{localize('com_auth_reset_password_success')}
|
{localize('com_auth_reset_password_success')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -53,7 +53,7 @@ function ResetPassword() {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||||
{localize('com_auth_reset_password')}
|
{localize('com_auth_reset_password')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -176,10 +176,13 @@ function ResetPassword() {
|
||||||
disabled={!!errors.password || !!errors.confirm_password}
|
disabled={!!errors.password || !!errors.confirm_password}
|
||||||
type="submit"
|
type="submit"
|
||||||
aria-label={localize('com_auth_submit_registration')}
|
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-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
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')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
<a href="/login" className="text-sm font-medium text-green-500">
|
||||||
|
{localize('com_auth_back_to_login')}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
78
client/src/components/Auth/SocialButton.tsx
Normal file
78
client/src/components/Auth/SocialButton.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isPressed, setIsPressed] = useState(false);
|
||||||
|
|
||||||
|
// New state to keep track of the currently pressed button
|
||||||
|
const [activeButton, setActiveButton] = useState(null);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
setIsHovered(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
setIsHovered(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = () => {
|
||||||
|
setIsPressed(true);
|
||||||
|
setActiveButton(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsPressed(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonStyles = () => {
|
||||||
|
const baseStyles = {
|
||||||
|
border: '1px solid #CCCCCC',
|
||||||
|
transition: 'background-color 0.3s ease, border 0.3s ease',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPressed && activeButton === id) {
|
||||||
|
return {
|
||||||
|
...baseStyles,
|
||||||
|
backgroundColor: '#B9DAE9',
|
||||||
|
border: '2px solid #B9DAE9',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHovered) {
|
||||||
|
return {
|
||||||
|
...baseStyles,
|
||||||
|
backgroundColor: '#E5E5E5',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseStyles,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 flex gap-x-2">
|
||||||
|
<a
|
||||||
|
aria-label={`${label}`}
|
||||||
|
className="justify-left flex w-full items-center space-x-3 rounded-md border px-5 py-3 transition-colors"
|
||||||
|
href={`${serverDomain}/oauth/${oauthPath}`}
|
||||||
|
data-testid={id}
|
||||||
|
style={getButtonStyles()}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
<p>{label}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SocialButton;
|
||||||
|
|
@ -31,13 +31,14 @@ const setup = ({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
data: {
|
data: {
|
||||||
googleLoginEnabled: true,
|
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
|
||||||
|
discordLoginEnabled: true,
|
||||||
facebookLoginEnabled: true,
|
facebookLoginEnabled: true,
|
||||||
|
githubLoginEnabled: true,
|
||||||
|
googleLoginEnabled: true,
|
||||||
openidLoginEnabled: true,
|
openidLoginEnabled: true,
|
||||||
openidLabel: 'Test OpenID',
|
openidLabel: 'Test OpenID',
|
||||||
openidImageUrl: 'http://test-server.com',
|
openidImageUrl: 'http://test-server.com',
|
||||||
githubLoginEnabled: true,
|
|
||||||
discordLoginEnabled: true,
|
|
||||||
registrationEnabled: true,
|
registrationEnabled: true,
|
||||||
emailLoginEnabled: true,
|
emailLoginEnabled: true,
|
||||||
socialLoginEnabled: true,
|
socialLoginEnabled: true,
|
||||||
|
|
@ -78,23 +79,23 @@ test('renders login form', () => {
|
||||||
expect(getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
|
expect(getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register');
|
expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register');
|
||||||
expect(getByRole('link', { name: /Login with Google/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Google/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Google/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/google',
|
'mock-server/oauth/google',
|
||||||
);
|
);
|
||||||
expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Facebook/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Facebook/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/facebook',
|
'mock-server/oauth/facebook',
|
||||||
);
|
);
|
||||||
expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Github/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Github/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/github',
|
'mock-server/oauth/github',
|
||||||
);
|
);
|
||||||
expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Discord/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Discord/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/discord',
|
'mock-server/oauth/discord',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,14 @@ const setup = ({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
data: {
|
data: {
|
||||||
googleLoginEnabled: true,
|
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
|
||||||
|
discordLoginEnabled: true,
|
||||||
facebookLoginEnabled: true,
|
facebookLoginEnabled: true,
|
||||||
|
githubLoginEnabled: true,
|
||||||
|
googleLoginEnabled: true,
|
||||||
openidLoginEnabled: true,
|
openidLoginEnabled: true,
|
||||||
openidLabel: 'Test OpenID',
|
openidLabel: 'Test OpenID',
|
||||||
openidImageUrl: 'http://test-server.com',
|
openidImageUrl: 'http://test-server.com',
|
||||||
githubLoginEnabled: true,
|
|
||||||
discordLoginEnabled: true,
|
|
||||||
emailLoginEnabled: true,
|
|
||||||
registrationEnabled: true,
|
registrationEnabled: true,
|
||||||
socialLoginEnabled: true,
|
socialLoginEnabled: true,
|
||||||
serverDomain: 'mock-server',
|
serverDomain: 'mock-server',
|
||||||
|
|
@ -85,23 +85,23 @@ test('renders registration form', () => {
|
||||||
expect(getByRole('button', { name: /Submit registration/i })).toBeInTheDocument();
|
expect(getByRole('button', { name: /Submit registration/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: 'Login' })).toBeInTheDocument();
|
expect(getByRole('link', { name: 'Login' })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: 'Login' })).toHaveAttribute('href', '/login');
|
expect(getByRole('link', { name: 'Login' })).toHaveAttribute('href', '/login');
|
||||||
expect(getByRole('link', { name: /Login with Google/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Google/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Google/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/google',
|
'mock-server/oauth/google',
|
||||||
);
|
);
|
||||||
expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Facebook/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Facebook/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/facebook',
|
'mock-server/oauth/facebook',
|
||||||
);
|
);
|
||||||
expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Github/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Github/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/github',
|
'mock-server/oauth/github',
|
||||||
);
|
);
|
||||||
expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Discord/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
|
expect(getByRole('link', { name: /Continue with Discord/i })).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'mock-server/oauth/discord',
|
'mock-server/oauth/discord',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,10 @@ export default {
|
||||||
com_auth_no_account: 'Don\'t have an account?',
|
com_auth_no_account: 'Don\'t have an account?',
|
||||||
com_auth_sign_up: 'Sign up',
|
com_auth_sign_up: 'Sign up',
|
||||||
com_auth_sign_in: 'Sign in',
|
com_auth_sign_in: 'Sign in',
|
||||||
com_auth_google_login: 'Login with Google',
|
com_auth_google_login: 'Continue with Google',
|
||||||
com_auth_facebook_login: 'Login with Facebook',
|
com_auth_facebook_login: 'Continue with Facebook',
|
||||||
com_auth_github_login: 'Login with Github',
|
com_auth_github_login: 'Continue with Github',
|
||||||
com_auth_discord_login: 'Login with Discord',
|
com_auth_discord_login: 'Continue with Discord',
|
||||||
com_auth_email: 'Email',
|
com_auth_email: 'Email',
|
||||||
com_auth_email_required: 'Email is required',
|
com_auth_email_required: 'Email is required',
|
||||||
com_auth_email_min_length: 'Email must be at least 6 characters',
|
com_auth_email_min_length: 'Email must be at least 6 characters',
|
||||||
|
|
@ -118,6 +118,7 @@ export default {
|
||||||
com_auth_to_try_again: 'to try again.',
|
com_auth_to_try_again: 'to try again.',
|
||||||
com_auth_submit_registration: 'Submit registration',
|
com_auth_submit_registration: 'Submit registration',
|
||||||
com_auth_welcome_back: 'Welcome back',
|
com_auth_welcome_back: 'Welcome back',
|
||||||
|
com_auth_back_to_login: 'Back to Login',
|
||||||
com_endpoint_open_menu: 'Open Menu',
|
com_endpoint_open_menu: 'Open Menu',
|
||||||
com_endpoint_bing_enable_sydney: 'Enable Sydney',
|
com_endpoint_bing_enable_sydney: 'Enable Sydney',
|
||||||
com_endpoint_bing_to_enable_sydney: 'To enable Sydney',
|
com_endpoint_bing_to_enable_sydney: 'To enable Sydney',
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,10 @@ export default {
|
||||||
com_auth_no_account: 'Non hai un account?',
|
com_auth_no_account: 'Non hai un account?',
|
||||||
com_auth_sign_up: 'Registrati',
|
com_auth_sign_up: 'Registrati',
|
||||||
com_auth_sign_in: 'Accedi',
|
com_auth_sign_in: 'Accedi',
|
||||||
com_auth_google_login: 'Accedi con Google',
|
com_auth_google_login: 'Continua con Google',
|
||||||
com_auth_facebook_login: 'Accedi con Facebook',
|
com_auth_facebook_login: 'Continua con Facebook',
|
||||||
com_auth_github_login: 'Accedi con Github',
|
com_auth_github_login: 'Continua con Github',
|
||||||
com_auth_discord_login: 'Accedi con Discord',
|
com_auth_discord_login: 'Continua con Discord',
|
||||||
com_auth_email: 'Email',
|
com_auth_email: 'Email',
|
||||||
com_auth_email_required: 'L\'email è obbligatoria',
|
com_auth_email_required: 'L\'email è obbligatoria',
|
||||||
com_auth_email_min_length: 'L\'email deve essere lunga almeno 6 caratteri',
|
com_auth_email_min_length: 'L\'email deve essere lunga almeno 6 caratteri',
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ module.exports = {
|
||||||
mono: ['Söhne Mono', 'monospace'],
|
mono: ['Söhne Mono', 'monospace'],
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
width: {
|
||||||
|
'authPageWidth': '370px',
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'accordion-down': {
|
'accordion-down': {
|
||||||
from: { height: 0 },
|
from: { height: 0 },
|
||||||
|
|
@ -51,6 +54,7 @@ module.exports = {
|
||||||
300: '#6dc8b9',
|
300: '#6dc8b9',
|
||||||
400: '#41a79d',
|
400: '#41a79d',
|
||||||
500: '#10a37f',
|
500: '#10a37f',
|
||||||
|
550: '#349072',
|
||||||
600: '#126e6b',
|
600: '#126e6b',
|
||||||
700: '#0a4f53',
|
700: '#0a4f53',
|
||||||
800: '#06373e',
|
800: '#06373e',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ Stay tuned for ongoing enhancements to customize your LibreChat instance!
|
||||||
- [Registration](#registration)
|
- [Registration](#registration)
|
||||||
- [Endpoints](#endpoints)
|
- [Endpoints](#endpoints)
|
||||||
- [Registration Object Structure](#registration-object-structure)
|
- [Registration Object Structure](#registration-object-structure)
|
||||||
|
- [**socialLogins**:](#socialLogins)
|
||||||
- [**allowedDomains**:](#allowedDomains)
|
- [**allowedDomains**:](#allowedDomains)
|
||||||
- [Custom Endpoint Object Structure](#custom-endpoint-object-structure)
|
- [Custom Endpoint Object Structure](#custom-endpoint-object-structure)
|
||||||
- [**name**:](#name)
|
- [**name**:](#name)
|
||||||
|
|
@ -112,6 +113,9 @@ docker-compose up # no need to rebuild
|
||||||
- **Key**: `registration`
|
- **Key**: `registration`
|
||||||
- **Type**: Object
|
- **Type**: Object
|
||||||
- **Description**: Configures registration-related settings for the application.
|
- **Description**: Configures registration-related settings for the application.
|
||||||
|
- **Sub-Key**: `socialLogins`
|
||||||
|
- **Type**: Array of Strings (`"google"`, `"facebook"`, `"openid"`, `"github"`, `"discord"`)
|
||||||
|
- **Description**: Determines both the available social login providers and their arranged order to list on the login/registration page, from top to bottom (first to last values). Note: a login option will not appear even if listed if not [properly configured.](./user_auth_system.md#social-authentication-setup-and-configuration)
|
||||||
- **Sub-Key**: `allowedDomains`
|
- **Sub-Key**: `allowedDomains`
|
||||||
- **Type**: Array of Strings
|
- **Type**: Array of Strings
|
||||||
- **Description**: Specifies a list of allowed email domains for user registration. Users attempting to register with email domains not listed here will be restricted from registering.
|
- **Description**: Specifies a list of allowed email domains for user registration. Users attempting to register with email domains not listed here will be restricted from registering.
|
||||||
|
|
@ -132,11 +136,23 @@ docker-compose up # no need to rebuild
|
||||||
```yaml
|
```yaml
|
||||||
# Example Registration Object Structure
|
# Example Registration Object Structure
|
||||||
registration:
|
registration:
|
||||||
|
socialLogins: ["google", "facebook", "github", "discord", "openid"]
|
||||||
allowedDomains:
|
allowedDomains:
|
||||||
- "gmail.com"
|
- "gmail.com"
|
||||||
- "protonmail.com"
|
- "protonmail.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### **socialLogins**:
|
||||||
|
|
||||||
|
> Defines the available social login providers and their display order.
|
||||||
|
|
||||||
|
- Type: Array of Strings
|
||||||
|
- Example:
|
||||||
|
```yaml
|
||||||
|
socialLogins: ["google", "facebook", "github", "discord", "openid"]
|
||||||
|
```
|
||||||
|
- **Note**: The order of the providers in the list determines their appearance order on the login/registration page. Each provider listed must be [properly configured](./user_auth_system.md#social-authentication-setup-and-configuration) within the system to be active and available for users. This configuration allows for a tailored authentication experience, emphasizing the most relevant or preferred social login options for your user base.
|
||||||
|
|
||||||
### **allowedDomains**:
|
### **allowedDomains**:
|
||||||
|
|
||||||
> A list specifying allowed email domains for registration.
|
> A list specifying allowed email domains for registration.
|
||||||
|
|
|
||||||
|
|
@ -666,7 +666,6 @@ see: **[User/Auth System](../configuration/user_auth_system.md)**
|
||||||
```bash
|
```bash
|
||||||
ALLOW_EMAIL_LOGIN=true
|
ALLOW_EMAIL_LOGIN=true
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
ALLOWED_REGISTRATION_DOMAINS=
|
|
||||||
ALLOW_SOCIAL_LOGIN=false
|
ALLOW_SOCIAL_LOGIN=false
|
||||||
ALLOW_SOCIAL_REGISTRATION=false
|
ALLOW_SOCIAL_REGISTRATION=false
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ version: 1.0.2
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
# Example Registration Object Structure (optional)
|
# Example Registration Object Structure (optional)
|
||||||
# registration:
|
registration:
|
||||||
|
socialLogins: ["github", "google", "discord", "openid", "facebook"]
|
||||||
# allowedDomains:
|
# allowedDomains:
|
||||||
# - "gmail.com"
|
# - "gmail.com"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.3.8",
|
"version": "0.3.9",
|
||||||
"description": "data services for librechat apps",
|
"description": "data services for librechat apps",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.es.js",
|
"module": "dist/index.es.js",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export const configSchema = z.object({
|
||||||
fileStrategy: fileSourceSchema.optional(),
|
fileStrategy: fileSourceSchema.optional(),
|
||||||
registration: z
|
registration: z
|
||||||
.object({
|
.object({
|
||||||
|
socialLogins: z.array(z.string()).optional(),
|
||||||
allowedDomains: z.array(z.string()).optional(),
|
allowedDomains: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
|
||||||
|
|
@ -189,13 +189,14 @@ export type TResetPassword = {
|
||||||
|
|
||||||
export type TStartupConfig = {
|
export type TStartupConfig = {
|
||||||
appTitle: string;
|
appTitle: string;
|
||||||
googleLoginEnabled: boolean;
|
socialLogins?: string[];
|
||||||
|
discordLoginEnabled: boolean;
|
||||||
facebookLoginEnabled: boolean;
|
facebookLoginEnabled: boolean;
|
||||||
openidLoginEnabled: boolean;
|
|
||||||
githubLoginEnabled: boolean;
|
githubLoginEnabled: boolean;
|
||||||
|
googleLoginEnabled: boolean;
|
||||||
|
openidLoginEnabled: boolean;
|
||||||
openidLabel: string;
|
openidLabel: string;
|
||||||
openidImageUrl: string;
|
openidImageUrl: string;
|
||||||
discordLoginEnabled: boolean;
|
|
||||||
serverDomain: string;
|
serverDomain: string;
|
||||||
emailLoginEnabled: boolean;
|
emailLoginEnabled: boolean;
|
||||||
registrationEnabled: boolean;
|
registrationEnabled: boolean;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue