mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
feat: Introduce PasswordInput Component for Enhanced Password Handling
- Added a new PasswordInput component to encapsulate password input logic, including visibility toggling and error handling. - Updated LoginForm, Registration, and ResetPassword components to utilize PasswordInput, improving code reusability and readability. - Removed redundant password input code from these components, streamlining the authentication forms. This change enhances user experience by providing a consistent and accessible password input interface across the application.
This commit is contained in:
parent
12f45c76ee
commit
b537450c57
5 changed files with 163 additions and 127 deletions
|
|
@ -7,6 +7,7 @@ import type { TAuthContext } from '~/common';
|
|||
import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider';
|
||||
import { validateEmail } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import PasswordInput from './PasswordInput';
|
||||
|
||||
type TLoginFormProps = {
|
||||
onSubmit: (data: TLoginUser) => void;
|
||||
|
|
@ -116,34 +117,20 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
|||
</div>
|
||||
{renderError('email')}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label={localize('com_auth_password')}
|
||||
{...register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: startupConfig?.minPasswordLength || 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
|
||||
>
|
||||
{localize('com_auth_password')}
|
||||
</label>
|
||||
</div>
|
||||
{renderError('password')}
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
label={localize('com_auth_password')}
|
||||
autoComplete="current-password"
|
||||
register={register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: startupConfig?.minPasswordLength || 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
||||
})}
|
||||
error={errors.password}
|
||||
/>
|
||||
{startupConfig.passwordResetEnabled && (
|
||||
<a
|
||||
href="/forgot-password"
|
||||
|
|
|
|||
68
client/src/components/Auth/PasswordInput.tsx
Normal file
68
client/src/components/Auth/PasswordInput.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import type { UseFormRegisterReturn, FieldError } from 'react-hook-form';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface PasswordInputProps {
|
||||
id: string;
|
||||
label: string;
|
||||
register: UseFormRegisterReturn;
|
||||
error?: FieldError;
|
||||
autoComplete?: string;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
const PasswordInput: React.FC<PasswordInputProps> = ({
|
||||
id,
|
||||
label,
|
||||
register,
|
||||
error,
|
||||
autoComplete = 'current-password',
|
||||
'data-testid': dataTestId,
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
id={id}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={label}
|
||||
{...register}
|
||||
aria-invalid={!!error}
|
||||
className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary pb-2.5 pe-10 ps-3.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none"
|
||||
placeholder=" "
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
className="absolute end-3 top-1/2 -translate-y-1/2 text-text-secondary transition-colors hover:text-text-primary"
|
||||
aria-label={localize(showPassword ? 'com_ui_hide_password' : 'com_ui_show_password')}
|
||||
>
|
||||
{showPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600 dark:text-red-500">
|
||||
{String(error.message)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
|
@ -9,6 +9,7 @@ import type { TRegisterUser, TError } from 'librechat-data-provider';
|
|||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { ErrorMessage } from './ErrorMessage';
|
||||
import PasswordInput from './PasswordInput';
|
||||
|
||||
const Registration: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -163,21 +164,35 @@ const Registration: React.FC = () => {
|
|||
message: localize('com_auth_email_pattern'),
|
||||
},
|
||||
})}
|
||||
{renderInput('password', 'com_auth_password', 'password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: startupConfig?.minPasswordLength || 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: string) =>
|
||||
value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
<PasswordInput
|
||||
id="password"
|
||||
label={localize('com_auth_password')}
|
||||
autoComplete="new-password"
|
||||
register={register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: startupConfig?.minPasswordLength || 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: localize('com_auth_password_max_length'),
|
||||
},
|
||||
})}
|
||||
error={errors.password}
|
||||
data-testid="password"
|
||||
/>
|
||||
<PasswordInput
|
||||
id="confirm_password"
|
||||
label={localize('com_auth_password_confirm')}
|
||||
autoComplete="new-password"
|
||||
register={register('confirm_password', {
|
||||
validate: (value) =>
|
||||
value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
error={errors.confirm_password}
|
||||
data-testid="confirm_password"
|
||||
/>
|
||||
|
||||
{startupConfig?.turnstile?.siteKey && (
|
||||
<div className="my-4 flex justify-center">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useResetPasswordMutation } from 'librechat-data-provider/react-query';
|
|||
import type { TResetPassword } from 'librechat-data-provider';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import PasswordInput from './PasswordInput';
|
||||
|
||||
function ResetPassword() {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -61,90 +62,54 @@ function ResetPassword() {
|
|||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="hidden"
|
||||
id="token"
|
||||
value={params.get('token') ?? ''}
|
||||
{...register('token', { required: 'Unable to process: No valid reset token' })}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="userId"
|
||||
value={params.get('userId') ?? ''}
|
||||
{...register('userId', { required: 'Unable to process: No valid user id' })}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label={localize('com_auth_password')}
|
||||
{...register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: startupConfig?.minPasswordLength || 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: localize('com_auth_password_max_length'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
|
||||
>
|
||||
{localize('com_auth_password')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{errors.password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.password.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
aria-label={localize('com_auth_password_confirm')}
|
||||
{...register('confirm_password', {
|
||||
validate: (value) => value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
htmlFor="confirm_password"
|
||||
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
|
||||
>
|
||||
{localize('com_auth_password_confirm')}
|
||||
</label>
|
||||
</div>
|
||||
{errors.confirm_password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.confirm_password.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.token && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.token.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.userId.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
id="token"
|
||||
value={params.get('token') ?? ''}
|
||||
{...register('token', { required: 'Unable to process: No valid reset token' })}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="userId"
|
||||
value={params.get('userId') ?? ''}
|
||||
{...register('userId', { required: 'Unable to process: No valid user id' })}
|
||||
/>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
label={localize('com_auth_password')}
|
||||
autoComplete="new-password"
|
||||
register={register('password', {
|
||||
required: localize('com_auth_password_required'),
|
||||
minLength: {
|
||||
value: startupConfig?.minPasswordLength || 8,
|
||||
message: localize('com_auth_password_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: localize('com_auth_password_max_length'),
|
||||
},
|
||||
})}
|
||||
error={errors.password}
|
||||
/>
|
||||
<PasswordInput
|
||||
id="confirm_password"
|
||||
label={localize('com_auth_password_confirm')}
|
||||
autoComplete="new-password"
|
||||
register={register('confirm_password', {
|
||||
validate: (value) => value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
error={errors.confirm_password}
|
||||
/>
|
||||
{errors.token && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.token.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{errors.userId.message}
|
||||
</span>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ export { default as VerifyEmail } from './VerifyEmail';
|
|||
export { default as ApiErrorWatcher } from './ApiErrorWatcher';
|
||||
export { default as RequestPasswordReset } from './RequestPasswordReset';
|
||||
export { default as TwoFactorScreen } from './TwoFactorScreen';
|
||||
export { default as PasswordInput } from './PasswordInput';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue