🔄🔐 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:
Marco Beretta 2024-02-05 09:31:18 +01:00 committed by GitHub
parent 25da90657d
commit a2c35e8415
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 536 additions and 532 deletions

View file

@ -56,13 +56,14 @@ describe.skip('GET /', () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({
appTitle: 'Test Title',
googleLoginEnabled: true,
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
googleLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
githubLoginEnabled: true,
discordLoginEnabled: true,
serverDomain: 'http://test-server.com',
emailLoginEnabled: 'true',
registrationEnabled: 'true',

View file

@ -10,18 +10,19 @@ router.get('/', async function (req, res) {
try {
const payload = {
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:
!!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:
!!process.env.OPENID_CLIENT_ID &&
!!process.env.OPENID_CLIENT_SECRET &&
!!process.env.OPENID_ISSUER &&
!!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,
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',
emailLoginEnabled,
registrationEnabled: isEnabled(process.env.ALLOW_REGISTRATION),

View file

@ -10,7 +10,15 @@ const paths = require('~/config/paths');
* @param {Express.Application} app - The Express application object.
*/
const AppService = async (app) => {
/** @type {TCustomConfig}*/
const config = (await loadCustomConfig()) ?? {};
const socialLogins = config.registration.socialLogins ?? [
'google',
'facebook',
'openid',
'github',
'discord',
];
const fileStrategy = config.fileStrategy ?? FileSources.local;
process.env.CDN_PROVIDER = fileStrategy;
@ -19,6 +27,7 @@ const AppService = async (app) => {
}
app.locals = {
socialLogins,
fileStrategy,
paths,
};

View file

@ -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.
* Validation via parsing the config file with the config schema.
* @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() {

View file

@ -6,12 +6,12 @@ import { useAuthContext } from '~/hooks/AuthContext';
import { getLoginError } from '~/utils';
import { useLocalize } from '~/hooks';
import LoginForm from './LoginForm';
import SocialButton from './SocialButton';
function Login() {
const { login, error, isAuthenticated } = useAuthContext();
const { data: startupConfig } = useGetStartupConfig();
const localize = useLocalize();
const navigate = useNavigate();
useEffect(() => {
@ -20,9 +20,79 @@ function Login() {
}
}, [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 (
<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">
{localize('com_auth_welcome_back')}
</h1>
@ -34,95 +104,28 @@ function Login() {
{localize(getLoginError(error))}
</div>
)}
{startupConfig?.emailLoginEnabled && <LoginForm onSubmit={login} />}
{startupConfig?.registrationEnabled && (
{startupConfig.emailLoginEnabled && <LoginForm onSubmit={login} />}
{startupConfig.registrationEnabled && (
<p className="my-4 text-center text-sm font-light text-gray-700">
{' '}
{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')}
</a>
</p>
)}
{startupConfig?.socialLoginEnabled && startupConfig?.emailLoginEnabled && (
{startupConfig.socialLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<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">
<a
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>
{startupConfig.emailLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<div className="absolute bg-white px-3 text-xs">Or</div>
</div>
<div className="mt-8" />
</>
)}
<div className="mt-2">
{socialLogins.map((provider) => providerComponents[provider] || null)}
</div>
</>
)}

View file

@ -1,3 +1,4 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useLocalize } from '~/hooks';
import { TLoginUser } from 'librechat-data-provider';
@ -6,15 +7,23 @@ type TLoginFormProps = {
onSubmit: (data: TLoginUser) => void;
};
function LoginForm({ onSubmit }: TLoginFormProps) {
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
const localize = useLocalize();
const {
register,
handleSubmit,
formState: { errors },
} = 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 (
<form
className="mt-6"
@ -31,23 +40,14 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
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'),
},
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"
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=" "
></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"
@ -55,12 +55,7 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
{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>
)}
{renderError('email')}
</div>
<div className="mb-2">
<div className="relative">
@ -71,19 +66,13 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
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'),
},
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"
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=" "
></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"
@ -91,15 +80,9 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
{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>
)}
{renderError('password')}
</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')}
</a>
<div className="mt-6">
@ -107,12 +90,13 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
aria-label="Sign in"
data-testid="login-button"
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')}
</button>
</div>
</form>
);
}
};
export default LoginForm;

View file

@ -1,15 +1,15 @@
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
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 { useLocalize } from '~/hooks';
import SocialButton from './SocialButton';
function Registration() {
const Registration: React.FC = () => {
const navigate = useNavigate();
const { data: startupConfig } = useGetStartupConfig();
const localize = useLocalize();
const {
@ -22,23 +22,20 @@ function Registration() {
const [error, setError] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>('');
const registerUser = useRegisterUserMutation();
const password = watch('password');
const onRegisterUserFormSubmit = (data: TRegisterUser) => {
registerUser.mutate(data, {
onSuccess: () => {
navigate('/c/new');
},
onError: (error) => {
setError(true);
const onRegisterUserFormSubmit = async (data: TRegisterUser) => {
try {
await registerUser.mutateAsync(data);
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
if (error.response?.data?.message) {
//@ts-ignore - error is of type unknown
setErrorMessage(error.response?.data?.message);
}
},
});
setErrorMessage(error.response?.data?.message);
}
}
};
useEffect(() => {
@ -47,9 +44,111 @@ function Registration() {
}
}, [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 (
<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">
{localize('com_auth_create_account')}
</h1>
@ -66,204 +165,61 @@ function Registration() {
className="mt-6"
aria-label="Registration form"
method="POST"
onSubmit={handleSubmit((data) => onRegisterUserFormSubmit(data))}
onSubmit={handleSubmit(onRegisterUserFormSubmit)}
>
<div className="mb-2">
<div className="relative">
<input
id="name"
type="text"
autoComplete="name"
aria-label={localize('com_auth_full_name')}
{...register('name', {
required: localize('com_auth_name_required'),
minLength: {
value: 3,
message: localize('com_auth_name_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_name_max_length'),
},
})}
aria-invalid={!!errors.name}
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="name"
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_full_name')}
</label>
</div>
{errors.name && (
<span role="alert" className="mt-1 text-sm text-black">
{/* @ts-ignore not sure why*/}
{errors.name.message}
</span>
)}
</div>
<div className="mb-2">
<div className="relative">
<input
type="text"
id="username"
aria-label={localize('com_auth_username')}
{...register('username', {
// required: localize('com_auth_username_required'),
minLength: {
value: 2,
message: localize('com_auth_username_min_length'),
},
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>
{renderInput('name', 'com_auth_full_name', 'text', {
required: localize('com_auth_name_required'),
minLength: {
value: 3,
message: localize('com_auth_name_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_name_max_length'),
},
})}
{renderInput('username', 'com_auth_username', 'text', {
minLength: {
value: 2,
message: localize('com_auth_username_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_username_max_length'),
},
})}
{renderInput('email', 'com_auth_email', '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'),
},
})}
{renderInput('password', 'com_auth_password', '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'),
},
})}
{renderInput('confirm_password', 'com_auth_password_confirm', 'password', {
validate: (value) => value === password || localize('com_auth_password_not_match'),
})}
<div className="mt-6">
<button
disabled={
!!errors.email ||
!!errors.name ||
!!errors.password ||
!!errors.username ||
!!errors.confirm_password
}
disabled={Object.keys(errors).length > 0}
type="submit"
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"
@ -273,7 +229,6 @@ function Registration() {
</div>
</form>
<p className="my-4 text-center text-sm font-light text-gray-700">
{' '}
{localize('com_auth_already_have_account')}{' '}
<a
href="/login"
@ -283,91 +238,24 @@ function Registration() {
{localize('com_auth_login')}
</a>
</p>
{startupConfig?.socialLoginEnabled && (
{startupConfig.socialLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<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">
<a
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>
{startupConfig.emailLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<div className="absolute bg-white px-3 text-xs">Or</div>
</div>
<div className="mt-8" />
</>
)}
<div className="mt-2">
{socialLogins.map((provider) => providerComponents[provider] || null)}
</div>
</>
)}
</div>
</div>
);
}
};
export default Registration;

View file

@ -61,9 +61,86 @@ function RequestPasswordReset() {
}
}, [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 (
<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>
{requestError && (
<div
@ -73,71 +150,7 @@ function RequestPasswordReset() {
{localize('com_auth_error_reset_password')}
</div>
)}
{bodyText ? (
<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>
)}
{renderFormContent()}
</div>
</div>
);

View file

@ -30,7 +30,7 @@ function ResetPassword() {
if (resetPassword.isSuccess) {
return (
<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">
{localize('com_auth_reset_password_success')}
</h1>
@ -53,7 +53,7 @@ function ResetPassword() {
} else {
return (
<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">
{localize('com_auth_reset_password')}
</h1>
@ -176,10 +176,13 @@ function ResetPassword() {
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-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')}
</button>
<a href="/login" className="text-sm font-medium text-green-500">
{localize('com_auth_back_to_login')}
</a>
</div>
</form>
</div>

View 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;

View file

@ -31,13 +31,14 @@ const setup = ({
isLoading: false,
isError: false,
data: {
googleLoginEnabled: true,
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
googleLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
githubLoginEnabled: true,
discordLoginEnabled: true,
registrationEnabled: true,
emailLoginEnabled: true,
socialLoginEnabled: true,
@ -78,23 +79,23 @@ test('renders login form', () => {
expect(getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register');
expect(getByRole('link', { name: /Login with Google/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Google/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Google/i })).toHaveAttribute(
'href',
'mock-server/oauth/google',
);
expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Facebook/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Facebook/i })).toHaveAttribute(
'href',
'mock-server/oauth/facebook',
);
expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Github/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Github/i })).toHaveAttribute(
'href',
'mock-server/oauth/github',
);
expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Discord/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Discord/i })).toHaveAttribute(
'href',
'mock-server/oauth/discord',
);

View file

@ -32,14 +32,14 @@ const setup = ({
isLoading: false,
isError: false,
data: {
googleLoginEnabled: true,
socialLogins: ['google', 'facebook', 'openid', 'github', 'discord'],
discordLoginEnabled: true,
facebookLoginEnabled: true,
githubLoginEnabled: true,
googleLoginEnabled: true,
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
githubLoginEnabled: true,
discordLoginEnabled: true,
emailLoginEnabled: true,
registrationEnabled: true,
socialLoginEnabled: true,
serverDomain: 'mock-server',
@ -85,23 +85,23 @@ test('renders registration form', () => {
expect(getByRole('button', { name: /Submit registration/i })).toBeInTheDocument();
expect(getByRole('link', { name: 'Login' })).toBeInTheDocument();
expect(getByRole('link', { name: 'Login' })).toHaveAttribute('href', '/login');
expect(getByRole('link', { name: /Login with Google/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Google/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Google/i })).toHaveAttribute(
'href',
'mock-server/oauth/google',
);
expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Facebook/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Facebook/i })).toHaveAttribute(
'href',
'mock-server/oauth/facebook',
);
expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Github/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Github/i })).toHaveAttribute(
'href',
'mock-server/oauth/github',
);
expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
expect(getByRole('link', { name: /Continue with Discord/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Continue with Discord/i })).toHaveAttribute(
'href',
'mock-server/oauth/discord',
);

View file

@ -71,10 +71,10 @@ export default {
com_auth_no_account: 'Don\'t have an account?',
com_auth_sign_up: 'Sign up',
com_auth_sign_in: 'Sign in',
com_auth_google_login: 'Login with Google',
com_auth_facebook_login: 'Login with Facebook',
com_auth_github_login: 'Login with Github',
com_auth_discord_login: 'Login with Discord',
com_auth_google_login: 'Continue with Google',
com_auth_facebook_login: 'Continue with Facebook',
com_auth_github_login: 'Continue with Github',
com_auth_discord_login: 'Continue with Discord',
com_auth_email: 'Email',
com_auth_email_required: 'Email is required',
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_submit_registration: 'Submit registration',
com_auth_welcome_back: 'Welcome back',
com_auth_back_to_login: 'Back to Login',
com_endpoint_open_menu: 'Open Menu',
com_endpoint_bing_enable_sydney: 'Enable Sydney',
com_endpoint_bing_to_enable_sydney: 'To enable Sydney',

View file

@ -71,10 +71,10 @@ export default {
com_auth_no_account: 'Non hai un account?',
com_auth_sign_up: 'Registrati',
com_auth_sign_in: 'Accedi',
com_auth_google_login: 'Accedi con Google',
com_auth_facebook_login: 'Accedi con Facebook',
com_auth_github_login: 'Accedi con Github',
com_auth_discord_login: 'Accedi con Discord',
com_auth_google_login: 'Continua con Google',
com_auth_facebook_login: 'Continua con Facebook',
com_auth_github_login: 'Continua con Github',
com_auth_discord_login: 'Continua con Discord',
com_auth_email: 'Email',
com_auth_email_required: 'L\'email è obbligatoria',
com_auth_email_min_length: 'L\'email deve essere lunga almeno 6 caratteri',

View file

@ -14,6 +14,9 @@ module.exports = {
mono: ['Söhne Mono', 'monospace'],
},
extend: {
width: {
'authPageWidth': '370px',
},
keyframes: {
'accordion-down': {
from: { height: 0 },
@ -51,6 +54,7 @@ module.exports = {
300: '#6dc8b9',
400: '#41a79d',
500: '#10a37f',
550: '#349072',
600: '#126e6b',
700: '#0a4f53',
800: '#06373e',

View file

@ -31,6 +31,7 @@ Stay tuned for ongoing enhancements to customize your LibreChat instance!
- [Registration](#registration)
- [Endpoints](#endpoints)
- [Registration Object Structure](#registration-object-structure)
- [**socialLogins**:](#socialLogins)
- [**allowedDomains**:](#allowedDomains)
- [Custom Endpoint Object Structure](#custom-endpoint-object-structure)
- [**name**:](#name)
@ -112,6 +113,9 @@ docker-compose up # no need to rebuild
- **Key**: `registration`
- **Type**: Object
- **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`
- **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.
@ -132,11 +136,23 @@ docker-compose up # no need to rebuild
```yaml
# Example Registration Object Structure
registration:
socialLogins: ["google", "facebook", "github", "discord", "openid"]
allowedDomains:
- "gmail.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**:
> A list specifying allowed email domains for registration.

View file

@ -666,7 +666,6 @@ see: **[User/Auth System](../configuration/user_auth_system.md)**
```bash
ALLOW_EMAIL_LOGIN=true
ALLOW_REGISTRATION=true
ALLOWED_REGISTRATION_DOMAINS=
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
```

View file

@ -5,7 +5,8 @@ version: 1.0.2
cache: true
# Example Registration Object Structure (optional)
# registration:
registration:
socialLogins: ["github", "google", "discord", "openid", "facebook"]
# allowedDomains:
# - "gmail.com"

View file

@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.3.8",
"version": "0.3.9",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View file

@ -32,6 +32,7 @@ export const configSchema = z.object({
fileStrategy: fileSourceSchema.optional(),
registration: z
.object({
socialLogins: z.array(z.string()).optional(),
allowedDomains: z.array(z.string()).optional(),
})
.optional(),

View file

@ -189,13 +189,14 @@ export type TResetPassword = {
export type TStartupConfig = {
appTitle: string;
googleLoginEnabled: boolean;
socialLogins?: string[];
discordLoginEnabled: boolean;
facebookLoginEnabled: boolean;
openidLoginEnabled: boolean;
githubLoginEnabled: boolean;
googleLoginEnabled: boolean;
openidLoginEnabled: boolean;
openidLabel: string;
openidImageUrl: string;
discordLoginEnabled: boolean;
serverDomain: string;
emailLoginEnabled: boolean;
registrationEnabled: boolean;