mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
F: Use zod validation for login form
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
This commit is contained in:
parent
7dda79e701
commit
c97b8997ce
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,21 +20,25 @@ 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);
|
||||||
|
const result = UserLoginSchema.safeParse({
|
||||||
|
username: formData.get('username'),
|
||||||
|
password: formData.get('password')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setValidationError(result.error);
|
||||||
|
} else {
|
||||||
|
login(result.data, () => {
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
router.back();
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,6 +47,12 @@ function LoginPage() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user