🔒 fix: resolve session persistence post password reset (#5077)

*  feat: Implement session management with CRUD operations and integrate into user workflows

*  refactor: Update session model import paths and enhance session creation logic in AuthService

*  refactor: Validate session and user ID formats in session management functions

*  style: Enhance UI components with improved styling and accessibility features

* chore: Update login form tests to use getByTestId instead of getByRole, remove console.log()

* chore: Update login form tests to use getByTestId instead of getByRole

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-12-23 11:12:07 +01:00 committed by GitHub
parent 9bca2ae953
commit bdb222d5f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 402 additions and 116 deletions

View file

@ -2,7 +2,7 @@ export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
<div
role="alert"
aria-live="assertive"
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
className="relative mt-6 rounded-lg border border-red-500/20 bg-red-50/50 px-6 py-4 text-red-700 shadow-sm transition-all dark:bg-red-950/30 dark:text-red-100"
>
{children}
</div>

View file

@ -26,7 +26,10 @@ function Login() {
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{' '}
{localize('com_auth_no_account')}{' '}
<a href="/register" className="p-1 text-green-500">
<a
href="/register"
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
>
{localize('com_auth_sign_up')}
</a>
</p>

View file

@ -153,16 +153,24 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
{renderError('password')}
</div>
{startupConfig.passwordResetEnabled && (
<a href="/forgot-password" className="text-sm text-green-500">
<a
href="/forgot-password"
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
>
{localize('com_auth_password_forgot')}
</a>
)}
<div className="mt-6">
<button
aria-label="Sign in"
aria-label={localize('com_auth_continue')}
data-testid="login-button"
type="submit"
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
className="
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
"
>
{localize('com_auth_continue')}
</button>

View file

@ -183,7 +183,12 @@ const Registration: React.FC = () => {
disabled={Object.keys(errors).length > 0}
type="submit"
aria-label="Submit registration"
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
className="
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
"
>
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
</button>
@ -192,7 +197,11 @@ const Registration: React.FC = () => {
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{localize('com_auth_already_have_account')}{' '}
<a href="/login" aria-label="Login" className="p-1 text-green-500">
<a
href="/login"
aria-label="Login"
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
>
{localize('com_auth_login')}
</a>
</p>

View file

@ -10,7 +10,7 @@ import { useLocalize } from '~/hooks';
const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
return (
<div
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700 dark:bg-green-900 dark:text-white"
className="relative mt-6 rounded-lg border border-green-500/20 bg-green-50/50 px-6 py-4 text-green-700 shadow-sm transition-all dark:bg-green-950/30 dark:text-green-100"
role="alert"
>
{children}
@ -21,13 +21,14 @@ const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
const ResetPasswordBodyText = () => {
const localize = useLocalize();
return (
<div className="flex flex-col">
{localize('com_auth_reset_password_if_email_exists')}
<span>
<a className="text-sm text-green-500 hover:underline" href="/login">
{localize('com_auth_back_to_login')}
</a>
</span>
<div className="flex flex-col space-y-4">
<p>{localize('com_auth_reset_password_if_email_exists')}</p>
<a
className="inline-flex text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
href="/login"
>
{localize('com_auth_back_to_login')}
</a>
</div>
);
};
@ -76,12 +77,12 @@ function RequestPasswordReset() {
return (
<form
className="mt-6"
className="mt-8 space-y-6"
aria-label="Password reset form"
method="POST"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-2">
<div className="space-y-2">
<div className="relative">
<input
type="email"
@ -105,42 +106,51 @@ function RequestPasswordReset() {
})}
aria-invalid={!!errors.email}
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
peer w-full rounded-lg border border-gray-300 bg-transparent px-4 py-3
text-base text-gray-900 placeholder-transparent transition-all
focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20
dark:border-gray-700 dark:text-white dark:focus:border-green-500
"
placeholder=" "
placeholder="email@example.com"
/>
<label
htmlFor="email"
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
absolute -top-2 left-2 z-10 bg-white px-2 text-sm text-gray-600
transition-all peer-placeholder-shown:top-3 peer-placeholder-shown:text-base
peer-placeholder-shown:text-gray-500 peer-focus:-top-2 peer-focus:text-sm
peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400
dark:peer-focus:text-green-500
"
>
{localize('com_auth_email_address')}
</label>
</div>
{errors.email && (
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
<p role="alert" className="text-sm font-medium text-red-600 dark:text-red-400">
{errors.email.message}
</span>
</p>
)}
</div>
<div className="mt-6">
<div className="space-y-4">
<button
type="submit"
disabled={!!errors.email}
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
className="
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
"
>
{localize('com_auth_continue')}
</button>
<div className="mt-4 flex justify-center">
<a href="/login" className="text-sm text-green-500">
{localize('com_auth_back_to_login')}
</a>
</div>
<a
href="/login"
className="block text-center text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
>
{localize('com_auth_back_to_login')}
</a>
</div>
</form>
);

View file

@ -35,7 +35,7 @@ function ResetPassword() {
return (
<>
<div
className="relative mb-8 mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700 dark:bg-gray-900 dark:text-white"
className="relative mb-8 mt-4 rounded-2xl border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700 dark:bg-gray-900 dark:text-white"
role="alert"
>
{localize('com_auth_login_with_new_password')}
@ -43,7 +43,7 @@ function ResetPassword() {
<button
onClick={() => navigate('/login')}
aria-label={localize('com_auth_sign_in')}
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
className="w-full transform rounded-2xl 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>
@ -163,7 +163,12 @@ function ResetPassword() {
disabled={!!errors.password || !!errors.confirm_password}
type="submit"
aria-label={localize('com_auth_submit_registration')}
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
className="
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
"
>
{localize('com_auth_continue')}
</button>

View file

@ -1,6 +1,6 @@
import reactRouter from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { render, waitFor } from 'test/layout-test-utils';
import { getByTestId, render, waitFor } from 'test/layout-test-utils';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
import AuthLayout from '~/components/Auth/AuthLayout';
@ -117,7 +117,7 @@ test('renders login form', () => {
const { getByLabelText, getByRole } = setup();
expect(getByLabelText(/email/i)).toBeInTheDocument();
expect(getByLabelText(/password/i)).toBeInTheDocument();
expect(getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
expect(getByTestId(document.body, 'login-button')).toBeInTheDocument();
expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument();
expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register');
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
@ -144,7 +144,7 @@ test('renders login form', () => {
test('calls loginUser.mutate on login', async () => {
const mutate = jest.fn();
const { getByLabelText, getByRole } = setup({
const { getByLabelText } = setup({
// @ts-ignore - we don't need all parameters of the QueryObserverResult
useLoginUserReturnValue: {
isLoading: false,
@ -155,7 +155,7 @@ test('calls loginUser.mutate on login', async () => {
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
const submitButton = getByTestId(document.body, 'login-button');
await userEvent.type(emailInput, 'test@test.com');
await userEvent.type(passwordInput, 'password');
@ -165,7 +165,7 @@ test('calls loginUser.mutate on login', async () => {
});
test('Navigates to / on successful login', async () => {
const { getByLabelText, getByRole, history } = setup({
const { getByLabelText, history } = setup({
// @ts-ignore - we don't need all parameters of the QueryObserverResult
useLoginUserReturnValue: {
isLoading: false,
@ -185,7 +185,7 @@ test('Navigates to / on successful login', async () => {
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
const submitButton = getByTestId(document.body, 'login-button');
await userEvent.type(emailInput, 'test@test.com');
await userEvent.type(passwordInput, 'password');

View file

@ -1,4 +1,4 @@
import { render } from 'test/layout-test-utils';
import { render, getByTestId } from 'test/layout-test-utils';
import userEvent from '@testing-library/user-event';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
@ -112,7 +112,7 @@ test('submits login form', async () => {
);
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
const submitButton = getByTestId(document.body, 'login-button');
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(passwordInput, 'password');
@ -127,7 +127,7 @@ test('displays validation error messages', async () => {
);
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
const submitButton = getByTestId(document.body, 'login-button');
await userEvent.type(emailInput, 'test');
await userEvent.type(passwordInput, 'pass');