diff --git a/rsconcept/frontend/src/app/Navigation/UserButton.tsx b/rsconcept/frontend/src/app/Navigation/UserButton.tsx index c4f69e5d..bd00f10e 100644 --- a/rsconcept/frontend/src/app/Navigation/UserButton.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserButton.tsx @@ -11,7 +11,6 @@ interface UserButtonProps { function UserButton({ onLogin, onClickUser }: UserButtonProps) { const { user, isAnonymous } = useAuthSuspense(); - console.log(user); const adminMode = usePreferencesStore(state => state.adminMode); if (isAnonymous) { return ( diff --git a/rsconcept/frontend/src/backend/auth/api.ts b/rsconcept/frontend/src/backend/auth/api.ts index 340a6eed..8f31b3d3 100644 --- a/rsconcept/frontend/src/backend/auth/api.ts +++ b/rsconcept/frontend/src/backend/auth/api.ts @@ -1,7 +1,7 @@ import { queryOptions } from '@tanstack/react-query'; import { z } from 'zod'; -import { axiosGet, axiosPost } from '@/backend/apiTransport'; +import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; import { ICurrentUser } from '@/models/user'; import { errors, information } from '@/utils/labels'; @@ -22,10 +22,25 @@ export type IUserLoginDTO = z.infer; /** * Represents data needed to update password for current user. */ -export interface IChangePasswordDTO { - old_password: string; - new_password: string; -} +export const ChangePasswordSchema = z + .object({ + old_password: z.string().nonempty(errors.requiredField), + new_password: z.string().nonempty(errors.requiredField), + new_password2: z.string().nonempty(errors.requiredField) + }) + .refine(schema => schema.new_password === schema.new_password2, { + path: ['new_password2'], + message: errors.passwordsMismatch + }) + .refine(schema => schema.old_password !== schema.new_password, { + path: ['new_password'], + message: errors.passwordsSame + }); + +/** + * Represents data needed to update password for current user. + */ +export type IChangePasswordDTO = z.infer; /** * Represents password reset request data. @@ -75,7 +90,7 @@ export const authApi = { request: { data: data } }), changePassword: (data: IChangePasswordDTO) => - axiosPost({ + axiosPatch({ endpoint: '/users/api/change-password', request: { data: data, diff --git a/rsconcept/frontend/src/backend/auth/useLogin.tsx b/rsconcept/frontend/src/backend/auth/useLogin.tsx index 97a42704..49158e6c 100644 --- a/rsconcept/frontend/src/backend/auth/useLogin.tsx +++ b/rsconcept/frontend/src/backend/auth/useLogin.tsx @@ -1,5 +1,4 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; import { authApi, IUserLoginDTO } from './api'; @@ -12,8 +11,7 @@ export const useLogin = () => { onSuccess: () => client.resetQueries() }); return { - login: (data: IUserLoginDTO, onSuccess?: () => void, onError?: (error: AxiosError) => void) => - mutation.mutate(data, { onSuccess, onError }), + login: (data: IUserLoginDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }), isPending: mutation.isPending, error: mutation.error, reset: mutation.reset diff --git a/rsconcept/frontend/src/components/ui/ErrorField.tsx b/rsconcept/frontend/src/components/ui/ErrorField.tsx index ed937b2c..9788a422 100644 --- a/rsconcept/frontend/src/components/ui/ErrorField.tsx +++ b/rsconcept/frontend/src/components/ui/ErrorField.tsx @@ -1,17 +1,24 @@ +import clsx from 'clsx'; import { FieldError, GlobalError } from 'react-hook-form'; -interface ErrorFieldProps { +import { CProps } from '../props'; + +interface ErrorFieldProps extends CProps.Styling { error?: FieldError | GlobalError; } /** * Displays an error message for input field. */ -function ErrorField({ error }: ErrorFieldProps) { +function ErrorField({ error, className, ...restProps }: ErrorFieldProps): React.ReactElement | null { if (!error) { return null; } - return
{error.message}
; + return ( +
+ {error.message} +
+ ); } export default ErrorField; diff --git a/rsconcept/frontend/src/components/ui/TextInput.tsx b/rsconcept/frontend/src/components/ui/TextInput.tsx index baf40ccd..b712f083 100644 --- a/rsconcept/frontend/src/components/ui/TextInput.tsx +++ b/rsconcept/frontend/src/components/ui/TextInput.tsx @@ -40,7 +40,7 @@ function TextInput({
- +
); } diff --git a/rsconcept/frontend/src/pages/LoginPage.tsx b/rsconcept/frontend/src/pages/LoginPage.tsx index e6334a2f..4ed1112b 100644 --- a/rsconcept/frontend/src/pages/LoginPage.tsx +++ b/rsconcept/frontend/src/pages/LoginPage.tsx @@ -12,7 +12,6 @@ import { useAuthSuspense } from '@/backend/auth/useAuth'; import { useLogin } from '@/backend/auth/useLogin'; import ExpectedAnonymous from '@/components/ExpectedAnonymous'; import { ErrorData } from '@/components/info/InfoError'; -import ErrorField from '@/components/ui/ErrorField'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; import TextURL from '@/components/ui/TextURL'; @@ -28,7 +27,6 @@ function LoginPage() { register, handleSubmit, clearErrors, - setError, resetField, formState: { errors } } = useForm({ @@ -37,25 +35,17 @@ function LoginPage() { }); const { isAnonymous } = useAuthSuspense(); - const { login, isPending, error: loginError, reset } = useLogin(); + const { login, isPending, error: serverError, reset } = useLogin(); function onSubmit(data: IUserLoginDTO) { - login( - data, - () => { - resetField('password'); - if (router.canBack()) { - router.back(); - } else { - router.push(urls.library); - } - }, - error => { - if (axios.isAxiosError(error) && error.response && error.response.status === 400) { - setError('root', { message: 'На Портале отсутствует такое сочетание имени пользователя и пароля' }); - } + login(data, () => { + resetField('password'); + if (router.canBack()) { + router.back(); + } else { + router.push(urls.library); } - ); + }); } function resetErrors() { @@ -99,8 +89,7 @@ function LoginPage() { - - + {serverError ? : null} ); } @@ -108,13 +97,13 @@ function LoginPage() { export default LoginPage; // ====== Internals ========= -function EscalateError({ error }: { error: ErrorData }): React.ReactElement | null { - // TODO: rework error escalation mechanism. Probably make it global. - if (!error) { - return null; - } +function ServerError({ error }: { error: ErrorData }): React.ReactElement | null { if (axios.isAxiosError(error) && error.response && error.response.status === 400) { - return null; + return ( +
+ На Портале отсутствует такое сочетание имени пользователя и пароля +
+ ); } throw error as Error; } diff --git a/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx b/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx index 6b5a8f58..26effbb8 100644 --- a/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx +++ b/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx @@ -39,8 +39,6 @@ function FormSignup() { resolver: zodResolver(UserSignupSchema) }); - const isValid = acceptPrivacy && acceptRules; - function resetErrors() { reset(); clearErrors(); @@ -57,6 +55,7 @@ function FormSignup() { function onSubmit(data: IUserSignupDTO) { signup(data, createdUser => router.push(urls.login_hint(createdUser.username))); } + return (
- +
{serverError ? : null} diff --git a/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx b/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx index 2e6a2017..b01b3ac6 100644 --- a/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx +++ b/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx @@ -1,92 +1,77 @@ 'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; import axios from 'axios'; import clsx from 'clsx'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; +import { useForm } from 'react-hook-form'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { urls } from '@/app/urls'; -import { IChangePasswordDTO } from '@/backend/auth/api'; +import { ChangePasswordSchema, IChangePasswordDTO } from '@/backend/auth/api'; import { useChangePassword } from '@/backend/auth/useChangePassword'; import { ErrorData } from '@/components/info/InfoError'; import FlexColumn from '@/components/ui/FlexColumn'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; -import { errors } from '@/utils/labels'; function EditorPassword() { const router = useConceptNavigation(); - const { changePassword, isPending, error, reset } = useChangePassword(); + const { changePassword, isPending, error: serverError, reset } = useChangePassword(); + const { + register, + handleSubmit, + clearErrors, + formState: { errors } + } = useForm({ + resolver: zodResolver(ChangePasswordSchema) + }); - const [oldPassword, setOldPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [newPasswordRepeat, setNewPasswordRepeat] = useState(''); + function resetErrors() { + reset(); + clearErrors(); + } - const passwordColor = - !!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'bg-warn-100' : 'clr-input'; - - const canSubmit = !!oldPassword && !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat; - - function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - if (newPassword !== newPasswordRepeat) { - toast.error(errors.passwordsMismatch); - return; - } - const data: IChangePasswordDTO = { - old_password: oldPassword, - new_password: newPassword - }; + function onSubmit(data: IChangePasswordDTO) { changePassword(data, () => router.push(urls.login)); } - useEffect(() => { - reset(); - }, [newPassword, oldPassword, newPasswordRepeat, reset]); - return ( void handleSubmit(onSubmit)(event)} + onChange={resetErrors} > setOldPassword(event.target.value)} + error={errors.old_password} /> { - setNewPassword(event.target.value); - }} + error={errors.new_password} /> { - setNewPasswordRepeat(event.target.value); - }} + error={errors.new_password2} /> - {error ? : null} + {serverError ? : null} - + ); } @@ -94,7 +79,7 @@ function EditorPassword() { export default EditorPassword; // ====== Internals ========= -function ProcessError({ error }: { error: ErrorData }): React.ReactElement { +function ServerError({ error }: { error: ErrorData }): React.ReactElement { if (axios.isAxiosError(error) && error.response && error.response.status === 400) { return
Неверно введен старый пароль
; } diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index e7676422..28aec17b 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -982,11 +982,12 @@ export const errors = { astFailed: 'Невозможно построить дерево разбора', typeStructureFailed: 'Структура отсутствует', passwordsMismatch: 'Пароли не совпадают', + passwordsSame: 'Пароль совпадает со старым', imageFailed: 'Ошибка при создании изображения', reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении', substituteInherited: 'Нельзя удалять наследованные конституенты при отождествлении', inputAlreadyExists: 'Концептуальная схема с таким именем уже существует', - requiredField: 'Поле обязательно для заполнения', + requiredField: 'Обязательное поле', emailField: 'Введите корректный адрес электронной почты', rulesNotAccepted: 'Примите условия пользования Порталом', privacyNotAccepted: 'Примите политику обработки персональных данных',