F: Use zod validation for login form
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run

This commit is contained in:
Ivan 2025-01-30 21:01:36 +03:00
parent 7dda79e701
commit c97b8997ce
3 changed files with 47 additions and 38 deletions

View File

@ -1,4 +1,5 @@
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { z } from 'zod';
import { axiosGet, axiosPost } from '@/backend/apiTransport'; import { axiosGet, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration'; import { DELAYS } from '@/backend/configuration';
@ -8,10 +9,15 @@ import { information } from '@/utils/labels';
/** /**
* Represents login data, used to authenticate users. * Represents login data, used to authenticate users.
*/ */
export interface IUserLoginDTO { export const UserLoginSchema = z.object({
username: string; username: z.string(),
password: string; password: z.string()
} });
/**
* Represents login data, used to authenticate users.
*/
export type IUserLoginDTO = z.infer<typeof UserLoginSchema>;
/** /**
* Represents data needed to update password for current user. * Represents data needed to update password for current user.

View File

@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { authApi } from './api'; import { authApi, IUserLoginDTO } from './api';
export const useLogin = () => { export const useLogin = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -13,11 +13,7 @@ export const useLogin = () => {
onSuccess: () => client.removeQueries({ queryKey: [libraryApi.baseKey] }) onSuccess: () => client.removeQueries({ queryKey: [libraryApi.baseKey] })
}); });
return { return {
login: ( login: (data: IUserLoginDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
username: string, //
password: string,
onSuccess?: () => void
) => mutation.mutate({ username, password }, { onSuccess }),
isPending: mutation.isPending, isPending: mutation.isPending,
error: mutation.error, error: mutation.error,
reset: mutation.reset reset: mutation.reset

View File

@ -2,10 +2,11 @@
import axios from 'axios'; import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { UserLoginSchema } from '@/backend/auth/api';
import { useAuthSuspense } from '@/backend/auth/useAuth'; import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useLogin } from '@/backend/auth/useLogin'; import { useLogin } from '@/backend/auth/useLogin';
import ExpectedAnonymous from '@/components/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/ExpectedAnonymous';
@ -19,30 +20,40 @@ import { resources } from '@/utils/constants';
function LoginPage() { function LoginPage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const initialName = query.get('username') ?? '';
const { isAnonymous } = useAuthSuspense(); const { isAnonymous } = useAuthSuspense();
const { login, isPending, error, reset } = useLogin(); const { login, isPending, error: loginError, reset } = useLogin();
const [validationError, setValidationError] = useState<ErrorData | undefined>(undefined);
const [username, setUsername] = useState(query.get('username') ?? '');
const [password, setPassword] = useState('');
useEffect(() => {
reset();
}, [username, password, reset]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!isPending) { if (!isPending) {
login(username, password, () => { const formData = new FormData(event.currentTarget);
if (router.canBack()) { const result = UserLoginSchema.safeParse({
router.back(); username: formData.get('username'),
} else { password: formData.get('password')
router.push(urls.library);
}
}); });
if (!result.success) {
setValidationError(result.error);
} else {
login(result.data, () => {
if (router.canBack()) {
router.back();
} else {
router.push(urls.library);
}
});
}
} }
} }
function resetErrors() {
reset();
setValidationError(undefined);
}
if (!isAnonymous) { if (!isAnonymous) {
return <ExpectedAnonymous />; return <ExpectedAnonymous />;
} }
@ -51,37 +62,33 @@ function LoginPage() {
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' /> <img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
<TextInput <TextInput
id='username' id='username'
label='Логин или email' name='username'
autoComplete='username' autoComplete='username'
label='Логин или email'
autoFocus autoFocus
required required
allowEnter allowEnter
spellCheck={false} spellCheck={false}
value={username} defaultValue={initialName}
onChange={event => setUsername(event.target.value)} onChange={resetErrors}
/> />
<TextInput <TextInput
id='password' id='password'
name='password'
type='password' type='password'
label='Пароль'
autoComplete='current-password' autoComplete='current-password'
label='Пароль'
required required
allowEnter allowEnter
value={password} onChange={resetErrors}
onChange={event => setPassword(event.target.value)}
/> />
<SubmitButton <SubmitButton text='Войти' className='self-center w-[12rem] mt-3' loading={isPending} />
text='Войти'
className='self-center w-[12rem] mt-3'
loading={isPending}
disabled={!username || !password}
/>
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>
<TextURL text='Восстановить пароль...' href='/restore-password' /> <TextURL text='Восстановить пароль...' href='/restore-password' />
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' /> <TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
</div> </div>
{error ? <ProcessError error={error} /> : null} {!!loginError || !!validationError ? <ProcessError error={loginError ?? validationError} /> : null}
</form> </form>
); );
} }