mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30: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.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',
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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,
|
||||
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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue