diff --git a/rsconcept/frontend/src/app/GlobalProviders.tsx b/rsconcept/frontend/src/app/GlobalProviders.tsx index e46b530b..2dd21953 100644 --- a/rsconcept/frontend/src/app/GlobalProviders.tsx +++ b/rsconcept/frontend/src/app/GlobalProviders.tsx @@ -6,7 +6,6 @@ import { ErrorBoundary } from 'react-error-boundary'; import { IntlProvider } from 'react-intl'; import { queryClient } from '@/backend/queryClient'; -import { AuthState } from '@/context/AuthContext'; import { GlobalOssState } from '@/context/GlobalOssContext'; import { LibraryState } from '@/context/LibraryContext'; @@ -33,7 +32,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) { > - @@ -42,7 +40,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) { - ); diff --git a/rsconcept/frontend/src/app/Navigation/Navigation.tsx b/rsconcept/frontend/src/app/Navigation/Navigation.tsx index 224c1231..571e7a87 100644 --- a/rsconcept/frontend/src/app/Navigation/Navigation.tsx +++ b/rsconcept/frontend/src/app/Navigation/Navigation.tsx @@ -29,7 +29,8 @@ function Navigation() { className={clsx( 'z-navigation', // prettier: split lines 'sticky top-0 left-0 right-0', - 'select-none' + 'select-none', + 'bg-prim-100' )} > @@ -57,6 +58,7 @@ function Navigation() { } onClick={navigateCreateNew} /> } onClick={navigateLibrary} /> } onClick={navigateHelp} /> + diff --git a/rsconcept/frontend/src/app/Navigation/UserButton.tsx b/rsconcept/frontend/src/app/Navigation/UserButton.tsx new file mode 100644 index 00000000..ff13baf0 --- /dev/null +++ b/rsconcept/frontend/src/app/Navigation/UserButton.tsx @@ -0,0 +1,35 @@ +import { useAuthSuspense } from '@/backend/auth/useAuth'; +import { IconLogin, IconUser2 } from '@/components/Icons'; +import { usePreferencesStore } from '@/stores/preferences'; + +import NavigationButton from './NavigationButton'; + +interface UserButtonProps { + onLogin: () => void; + onClickUser: () => void; +} + +function UserButton({ onLogin, onClickUser }: UserButtonProps) { + const { user } = useAuthSuspense(); + const adminMode = usePreferencesStore(state => state.adminMode); + if (!user) { + return ( + } + onClick={onLogin} + /> + ); + } else { + return ( + } + onClick={onClickUser} + /> + ); + } +} + +export default UserButton; diff --git a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx index 4284f515..16b77812 100644 --- a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx @@ -1,3 +1,5 @@ +import { useAuth } from '@/backend/auth/useAuth'; +import { useLogout } from '@/backend/auth/useLogout'; import { IconAdmin, IconAdminOff, @@ -15,7 +17,6 @@ import { import { CProps } from '@/components/props'; import Dropdown from '@/components/ui/Dropdown'; import DropdownButton from '@/components/ui/DropdownButton'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { usePreferencesStore } from '@/stores/preferences'; @@ -28,7 +29,8 @@ interface UserDropdownProps { function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { const router = useConceptNavigation(); - const { user, logout } = useAuth(); + const { user } = useAuth(); + const { logout } = useLogout(); const darkMode = usePreferencesStore(state => state.darkMode); const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode); diff --git a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx index 236336da..a36499aa 100644 --- a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx @@ -1,41 +1,22 @@ -import { IconLogin, IconUser2 } from '@/components/Icons'; +import { Suspense } from 'react'; + import Loader from '@/components/ui/Loader'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import useDropdown from '@/hooks/useDropdown'; -import { usePreferencesStore } from '@/stores/preferences'; import { urls } from '../urls'; -import NavigationButton from './NavigationButton'; +import UserButton from './UserButton'; import UserDropdown from './UserDropdown'; function UserMenu() { const router = useConceptNavigation(); - const { user, loading } = useAuth(); - const adminMode = usePreferencesStore(state => state.adminMode); const menu = useDropdown(); - - const navigateLogin = () => router.push(urls.login); - return (
- {loading ? : null} - {!user && !loading ? ( - } - onClick={navigateLogin} - /> - ) : null} - {user && !loading ? ( - } - onClick={menu.toggle} - /> - ) : null} - menu.hide()} /> + }> + router.push(urls.login)} onClickUser={menu.toggle} /> + + menu.hide()} />
); } diff --git a/rsconcept/frontend/src/backend/apiTransport.ts b/rsconcept/frontend/src/backend/apiTransport.ts index 3e29bc54..af21e549 100644 --- a/rsconcept/frontend/src/backend/apiTransport.ts +++ b/rsconcept/frontend/src/backend/apiTransport.ts @@ -42,17 +42,17 @@ export interface IAxiosRequest { // ================ Transport API calls ================ export function AxiosGet({ endpoint, request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .get(endpoint, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } @@ -61,17 +61,17 @@ export function AxiosPost({ request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .post(endpoint, request.data, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } @@ -80,17 +80,17 @@ export function AxiosDelete({ request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .delete(endpoint, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } @@ -99,17 +99,17 @@ export function AxiosPatch({ request, options }: IAxiosRequest) { - if (request.setLoading) request.setLoading(true); + request.setLoading?.(true); axiosInstance .patch(endpoint, request.data, options) .then(response => { - if (request.setLoading) request.setLoading(false); - if (request.onSuccess) request.onSuccess(response.data); + request.setLoading?.(false); + request.onSuccess?.(response.data); return response.data; }) .catch((error: Error | AxiosError) => { - if (request.setLoading) request.setLoading(false); + request.setLoading?.(false); if (request.showError) toast.error(extractErrorMessage(error)); - if (request.onError) request.onError(error); + request.onError?.(error); }); } diff --git a/rsconcept/frontend/src/backend/auth/api.ts b/rsconcept/frontend/src/backend/auth/api.ts new file mode 100644 index 00000000..4d543c1d --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/api.ts @@ -0,0 +1,67 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { ICurrentUser, IUser } from '@/models/user'; + +import { axiosInstance } from '../apiConfiguration'; + +/** + * Represents login data, used to authenticate users. + */ +export interface IUserLoginDTO { + username: string; + password: string; +} + +/** + * Represents data needed to update password for current user. + */ +export interface IChangePasswordDTO { + old_password: string; + new_password: string; +} + +/** + * Represents password reset request data. + */ +export interface IRequestPasswordDTO extends Pick {} + +/** + * Represents password reset data. + */ +export interface IResetPasswordDTO { + password: string; + token: string; +} + +/** + * Represents password token data. + */ +export interface IPasswordTokenDTO extends Pick {} + +/** + * Authentication API. + */ +export const authApi = { + baseKey: 'auth', + + getAuthQueryOptions: () => { + return queryOptions({ + queryKey: [authApi.baseKey, 'user'], + queryFn: meta => + axiosInstance + .get('/users/api/auth', { + signal: meta.signal + }) + .then(response => (response.data.id === null ? null : response.data)), + placeholderData: null, + staleTime: 24 * 60 * 60 * 1000 + }); + }, + + logout: () => axiosInstance.post('/users/api/logout'), + login: (data: IUserLoginDTO) => axiosInstance.post('/users/api/login', data), + changePassword: (data: IChangePasswordDTO) => axiosInstance.post('/users/api/change-password', data), + requestPasswordReset: (data: IRequestPasswordDTO) => axiosInstance.post('/users/api/password-reset', data), + validatePasswordToken: (data: IPasswordTokenDTO) => axiosInstance.post('/users/api/password-reset/validate', data), + resetPassword: (data: IResetPasswordDTO) => axiosInstance.post('/users/api/password-reset/confirm', data) +}; diff --git a/rsconcept/frontend/src/backend/auth/useAuth.tsx b/rsconcept/frontend/src/backend/auth/useAuth.tsx new file mode 100644 index 00000000..14f86f2e --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useAuth.tsx @@ -0,0 +1,21 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { authApi } from './api'; + +export function useAuth() { + const { + data: user, + isLoading, + error + } = useQuery({ + ...authApi.getAuthQueryOptions() + }); + return { user, isLoading, error }; +} + +export function useAuthSuspense() { + const { data: user } = useSuspenseQuery({ + ...authApi.getAuthQueryOptions() + }); + return { user }; +} diff --git a/rsconcept/frontend/src/backend/auth/useChangePassword.tsx b/rsconcept/frontend/src/backend/auth/useChangePassword.tsx new file mode 100644 index 00000000..79c00fc5 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useChangePassword.tsx @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi, IChangePasswordDTO } from './api'; + +export const useChangePassword = () => { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['change-password'], + mutationFn: authApi.changePassword, + onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { + changePassword: (data: IChangePasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useLogin.tsx b/rsconcept/frontend/src/backend/auth/useLogin.tsx new file mode 100644 index 00000000..9924a2ad --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useLogin.tsx @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi } from './api'; + +export const useLogin = () => { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['login'], + mutationFn: authApi.login, + onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { + login: (username: string, password: string, onSuccess?: () => void) => + mutation.mutate({ username, password }, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useLogout.tsx b/rsconcept/frontend/src/backend/auth/useLogout.tsx new file mode 100644 index 00000000..f0f9b252 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useLogout.tsx @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi } from './api'; + +export const useLogout = () => { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['logout'], + mutationFn: authApi.logout, + onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useRequestPasswordReset.tsx b/rsconcept/frontend/src/backend/auth/useRequestPasswordReset.tsx new file mode 100644 index 00000000..f31d5118 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useRequestPasswordReset.tsx @@ -0,0 +1,16 @@ +import { useMutation } from '@tanstack/react-query'; + +import { authApi, IRequestPasswordDTO } from './api'; + +export const useRequestPasswordReset = () => { + const mutation = useMutation({ + mutationKey: ['request-password-reset'], + mutationFn: authApi.requestPasswordReset + }); + return { + requestPasswordReset: (data: IRequestPasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/auth/useResetPassword.tsx b/rsconcept/frontend/src/backend/auth/useResetPassword.tsx new file mode 100644 index 00000000..683c05c6 --- /dev/null +++ b/rsconcept/frontend/src/backend/auth/useResetPassword.tsx @@ -0,0 +1,24 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api'; + +export const useResetPassword = () => { + const queryClient = useQueryClient(); + const validateMutation = useMutation({ + mutationKey: ['reset-password'], + mutationFn: authApi.validatePasswordToken, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + const resetMutation = useMutation({ + mutationKey: ['reset-password'], + mutationFn: authApi.resetPassword, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) + }); + return { + validateToken: (data: IPasswordTokenDTO, onSuccess?: () => void) => validateMutation.mutate(data, { onSuccess }), + resetPassword: (data: IResetPasswordDTO, onSuccess?: () => void) => resetMutation.mutate(data, { onSuccess }), + isPending: resetMutation.isPending, + error: resetMutation.error, + reset: resetMutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/users.ts b/rsconcept/frontend/src/backend/users.ts deleted file mode 100644 index eda6ef97..00000000 --- a/rsconcept/frontend/src/backend/users.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Endpoints: users. - */ - -import { - ICurrentUser, - IPasswordTokenData, - IRequestPasswordData, - IResetPasswordData, - IUserInfo, - IUserLoginData, - IUserProfile, - IUserSignupData, - IUserUpdateData, - IUserUpdatePassword -} from '@/models/user'; - -import { AxiosGet, AxiosPatch, AxiosPost, FrontAction, FrontExchange, FrontPull, FrontPush } from './apiTransport'; - -export function getAuth(request: FrontPull) { - AxiosGet({ - endpoint: `/users/api/auth`, - request: request - }); -} - -export function postLogin(request: FrontPush) { - AxiosPost({ - endpoint: '/users/api/login', - request: request - }); -} - -export function postLogout(request: FrontAction) { - AxiosPost({ - endpoint: '/users/api/logout', - request: request - }); -} - -export function postSignup(request: FrontExchange) { - AxiosPost({ - endpoint: '/users/api/signup', - request: request - }); -} - -export function getProfile(request: FrontPull) { - AxiosGet({ - endpoint: '/users/api/profile', - request: request - }); -} - -export function patchProfile(request: FrontExchange) { - AxiosPatch({ - endpoint: '/users/api/profile', - request: request - }); -} - -export function patchPassword(request: FrontPush) { - AxiosPatch({ - endpoint: '/users/api/change-password', - request: request - }); -} - -export function postRequestPasswordReset(request: FrontPush) { - // title: 'Request password reset', - AxiosPost({ - endpoint: '/users/api/password-reset', - request: request - }); -} - -export function postValidatePasswordToken(request: FrontPush) { - // title: 'Validate password token', - AxiosPost({ - endpoint: '/users/api/password-reset/validate', - request: request - }); -} - -export function postResetPassword(request: FrontPush) { - // title: 'Reset password', - AxiosPost({ - endpoint: '/users/api/password-reset/confirm', - request: request - }); -} - -export function getActiveUsers(request: FrontPull) { - // title: 'Active users list', - AxiosGet({ - endpoint: '/users/api/active-users', - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/users/api.ts b/rsconcept/frontend/src/backend/users/api.ts index ffabbd30..b20983e9 100644 --- a/rsconcept/frontend/src/backend/users/api.ts +++ b/rsconcept/frontend/src/backend/users/api.ts @@ -1,18 +1,40 @@ import { queryOptions } from '@tanstack/react-query'; -import { IUserInfo } from '@/models/user'; +import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user'; import { axiosInstance } from '../apiConfiguration'; +/** + * Represents user data, intended to update user profile in persistent storage. + */ +export interface IUpdateProfileDTO extends Omit {} + export const usersApi = { baseKey: 'users', - getUsersQueryOptions: () => { - return queryOptions({ + getUsersQueryOptions: () => + queryOptions({ queryKey: [usersApi.baseKey, 'list'], queryFn: meta => - axiosInstance.get(`/users/api/active-users`, { - signal: meta.signal - }) - }); - } + axiosInstance + .get('/users/api/active-users', { + signal: meta.signal + }) + .then(response => response.data), + placeholderData: [] + }), + getProfileQueryOptions: () => + queryOptions({ + queryKey: [usersApi.baseKey, 'profile'], + queryFn: meta => + axiosInstance + .get('/users/api/profile', { + signal: meta.signal + }) + .then(response => response.data) + }), + + signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data), + updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data) }; + +//DataCallback diff --git a/rsconcept/frontend/src/backend/users/useProfile.tsx b/rsconcept/frontend/src/backend/users/useProfile.tsx new file mode 100644 index 00000000..f74802a3 --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useProfile.tsx @@ -0,0 +1,21 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +import { usersApi } from './api'; + +export function useProfile() { + const { + data: profile, + isLoading, + error + } = useQuery({ + ...usersApi.getProfileQueryOptions() + }); + return { profile, isLoading, error }; +} + +export function useProfileSuspense() { + const { data: profile } = useSuspenseQuery({ + ...usersApi.getProfileQueryOptions() + }); + return { profile }; +} diff --git a/rsconcept/frontend/src/backend/users/useSignup.tsx b/rsconcept/frontend/src/backend/users/useSignup.tsx new file mode 100644 index 00000000..777c6fcf --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useSignup.tsx @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { usersApi } from '@/backend/users/api'; +import { IUserProfile, IUserSignupData } from '@/models/user'; + +import { DataCallback } from '../apiTransport'; + +export const useSignup = () => { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['signup'], + mutationFn: usersApi.signup, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [usersApi.baseKey] }) + }); + return { + signup: (data: IUserSignupData, onSuccess?: DataCallback) => + mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/users/useUpdateProfile.tsx b/rsconcept/frontend/src/backend/users/useUpdateProfile.tsx new file mode 100644 index 00000000..051f5ac1 --- /dev/null +++ b/rsconcept/frontend/src/backend/users/useUpdateProfile.tsx @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { IUserProfile } from '@/models/user'; + +import { IUpdateProfileDTO, usersApi } from './api'; + +// TODO: reload users / optimistic update + +export const useUpdateProfile = () => { + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationKey: ['update-profile'], + mutationFn: usersApi.updateProfile, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [usersApi.baseKey] }) + }); + return { + updateProfile: (data: IUpdateProfileDTO, onSuccess?: (newUser: IUserProfile) => void) => + mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }), + isPending: mutation.isPending, + error: mutation.error, + reset: mutation.reset + }; +}; diff --git a/rsconcept/frontend/src/backend/users/useUsers.tsx b/rsconcept/frontend/src/backend/users/useUsers.tsx index 8312fe89..d4172871 100644 --- a/rsconcept/frontend/src/backend/users/useUsers.tsx +++ b/rsconcept/frontend/src/backend/users/useUsers.tsx @@ -3,17 +3,16 @@ import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { usersApi } from './api'; export function useUsersSuspense() { - const { data: users, refetch } = useSuspenseQuery({ - ...usersApi.getUsersQueryOptions() + const { data: users } = useSuspenseQuery({ + ...usersApi.getUsersQueryOptions(), + initialData: [] }); - - return { users: users?.data ?? [], refetch }; + return { users }; } export function useUsers() { - const { data: users, refetch } = useQuery({ + const { data: users } = useQuery({ ...usersApi.getUsersQueryOptions() }); - - return { users: users?.data ?? [], refetch }; + return { users: users ?? [] }; } diff --git a/rsconcept/frontend/src/components/ui/SubmitButton.tsx b/rsconcept/frontend/src/components/ui/SubmitButton.tsx index 815ba686..6cf3911e 100644 --- a/rsconcept/frontend/src/components/ui/SubmitButton.tsx +++ b/rsconcept/frontend/src/components/ui/SubmitButton.tsx @@ -29,7 +29,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...re loading && 'cursor-progress', className )} - disabled={disabled ?? loading} + disabled={disabled || loading} {...restProps} > {icon ? {icon} : null} diff --git a/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx b/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx index ce5f9428..6889419d 100644 --- a/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx +++ b/rsconcept/frontend/src/components/wrap/ExpectedAnonymous.tsx @@ -1,11 +1,13 @@ import { urls } from '@/app/urls'; -import { useAuth } from '@/context/AuthContext'; +import { useAuth } from '@/backend/auth/useAuth'; +import { useLogout } from '@/backend/auth/useLogout'; import { useConceptNavigation } from '@/context/NavigationContext'; import TextURL from '../ui/TextURL'; function ExpectedAnonymous() { - const { user, logout } = useAuth(); + const { user } = useAuth(); + const { logout } = useLogout(); const router = useConceptNavigation(); function logoutAndRedirect() { diff --git a/rsconcept/frontend/src/components/wrap/RequireAuth.tsx b/rsconcept/frontend/src/components/wrap/RequireAuth.tsx index f9b3bcfa..280ef4c2 100644 --- a/rsconcept/frontend/src/components/wrap/RequireAuth.tsx +++ b/rsconcept/frontend/src/components/wrap/RequireAuth.tsx @@ -1,14 +1,14 @@ 'use client'; -import { useAuth } from '@/context/AuthContext'; +import { useAuth } from '@/backend/auth/useAuth'; import Loader from '../ui/Loader'; import TextURL from '../ui/TextURL'; function RequireAuth({ children }: React.PropsWithChildren) { - const { user, loading } = useAuth(); + const { user, isLoading } = useAuth(); - if (loading) { + if (isLoading) { return ; } if (user) { diff --git a/rsconcept/frontend/src/context/AuthContext.tsx b/rsconcept/frontend/src/context/AuthContext.tsx deleted file mode 100644 index d8fad021..00000000 --- a/rsconcept/frontend/src/context/AuthContext.tsx +++ /dev/null @@ -1,204 +0,0 @@ -'use client'; - -import { createContext, useCallback, useContext, useEffect, useState } from 'react'; - -import { DataCallback } from '@/backend/apiTransport'; -import { - getAuth, - patchPassword, - postLogin, - postLogout, - postRequestPasswordReset, - postResetPassword, - postSignup, - postValidatePasswordToken -} from '@/backend/users'; -import { type ErrorData } from '@/components/info/InfoError'; -import { - ICurrentUser, - IPasswordTokenData, - IRequestPasswordData, - IResetPasswordData, - IUserLoginData, - IUserProfile, - IUserSignupData, - IUserUpdatePassword -} from '@/models/user'; -import { contextOutsideScope } from '@/utils/labels'; - -interface IAuthContext { - user: ICurrentUser | undefined; - login: (data: IUserLoginData, callback?: DataCallback) => void; - logout: (callback?: DataCallback) => void; - signup: (data: IUserSignupData, callback?: DataCallback) => void; - updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void; - requestPasswordReset: (data: IRequestPasswordData, callback?: () => void) => void; - validateToken: (data: IPasswordTokenData, callback?: () => void) => void; - resetPassword: (data: IResetPasswordData, callback?: () => void) => void; - loading: boolean; - error: ErrorData; - setError: (error: ErrorData) => void; -} - -const AuthContext = createContext(null); -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error(contextOutsideScope('useAuth', 'AuthState')); - } - return context; -}; - -export const AuthState = ({ children }: React.PropsWithChildren) => { - const [user, setUser] = useState(undefined); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(undefined); - - const reload = useCallback( - (callback?: () => void) => { - getAuth({ - onError: () => setUser(undefined), - setLoading: setLoading, - onSuccess: currentUser => { - if (currentUser.id) { - setUser(currentUser); - } else { - setUser(undefined); - } - callback?.(); - } - }); - }, - [setUser] - ); - - function login(data: IUserLoginData, callback?: DataCallback) { - setError(undefined); - postLogin({ - data: data, - showError: false, - setLoading: setLoading, - onError: setError, - onSuccess: newData => - reload(() => { - callback?.(newData); - }) - }); - } - - function logout(callback?: DataCallback) { - setError(undefined); - postLogout({ - showError: true, - onSuccess: newData => - reload(() => { - callback?.(newData); - }) - }); - } - - function signup(data: IUserSignupData, callback?: DataCallback) { - setError(undefined); - postSignup({ - data: data, - showError: true, - setLoading: setLoading, - onError: setError, - onSuccess: newData => - reload(() => { - // TODO: reload users / optimistic update - // users.push(newData as IUserInfo); - callback?.(newData); - }) - }); - } - - const updatePassword = useCallback( - (data: IUserUpdatePassword, callback?: () => void) => { - setError(undefined); - patchPassword({ - data: data, - showError: true, - setLoading: setLoading, - onError: setError, - onSuccess: () => reload(callback) - }); - }, - [reload] - ); - - const requestPasswordReset = useCallback( - (data: IRequestPasswordData, callback?: () => void) => { - setError(undefined); - postRequestPasswordReset({ - data: data, - showError: false, - setLoading: setLoading, - onError: setError, - onSuccess: () => - reload(() => { - callback?.(); - }) - }); - }, - [reload] - ); - - const validateToken = useCallback( - (data: IPasswordTokenData, callback?: () => void) => { - setError(undefined); - postValidatePasswordToken({ - data: data, - showError: false, - setLoading: setLoading, - onError: setError, - onSuccess: () => - reload(() => { - callback?.(); - }) - }); - }, - [reload] - ); - - const resetPassword = useCallback( - (data: IResetPasswordData, callback?: () => void) => { - setError(undefined); - postResetPassword({ - data: data, - showError: false, - setLoading: setLoading, - onError: setError, - onSuccess: () => - reload(() => { - callback?.(); - }) - }); - }, - [reload] - ); - - useEffect(() => { - reload(); - }, [reload]); - - return ( - - {children} - - ); -}; diff --git a/rsconcept/frontend/src/context/LibraryContext.tsx b/rsconcept/frontend/src/context/LibraryContext.tsx index e087c0ab..e212f467 100644 --- a/rsconcept/frontend/src/context/LibraryContext.tsx +++ b/rsconcept/frontend/src/context/LibraryContext.tsx @@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { DataCallback } from '@/backend/apiTransport'; +import { useAuth } from '@/backend/auth/useAuth'; import { deleteLibraryItem, getAdminLibrary, @@ -24,8 +25,6 @@ import { RSFormLoader } from '@/models/RSFormLoader'; import { usePreferencesStore } from '@/stores/preferences'; import { contextOutsideScope } from '@/utils/labels'; -import { useAuth } from './AuthContext'; - interface ILibraryContext { items: ILibraryItem[]; templates: ILibraryItem[]; @@ -62,7 +61,7 @@ export const useLibrary = (): ILibraryContext => { }; export const LibraryState = ({ children }: React.PropsWithChildren) => { - const { user, loading: userLoading } = useAuth(); + const { user, isLoading: userLoading } = useAuth(); const adminMode = usePreferencesStore(state => state.adminMode); const [items, setItems] = useState([]); diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index c13d5b6e..9bc96fae 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { DataCallback } from '@/backend/apiTransport'; +import { useAuth } from '@/backend/auth/useAuth'; import { patchLibraryItem, patchSetAccessPolicy, @@ -38,7 +39,6 @@ import { import { UserID } from '@/models/user'; import { contextOutsideScope } from '@/utils/labels'; -import { useAuth } from './AuthContext'; import { useGlobalOss } from './GlobalOssContext'; import { useLibrary } from './LibraryContext'; diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 11533474..5148c11c 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { DataCallback } from '@/backend/apiTransport'; +import { useAuth } from '@/backend/auth/useAuth'; import { patchLibraryItem, patchSetAccessPolicy, @@ -50,7 +51,6 @@ import { import { UserID } from '@/models/user'; import { contextOutsideScope } from '@/utils/labels'; -import { useAuth } from './AuthContext'; import { useGlobalOss } from './GlobalOssContext'; import { useLibrary } from './LibraryContext'; diff --git a/rsconcept/frontend/src/context/UserProfileContext.tsx b/rsconcept/frontend/src/context/UserProfileContext.tsx deleted file mode 100644 index 392a9f2e..00000000 --- a/rsconcept/frontend/src/context/UserProfileContext.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import { createContext, useCallback, useContext, useEffect, useState } from 'react'; - -import { DataCallback } from '@/backend/apiTransport'; -import { getProfile, patchProfile } from '@/backend/users'; -import { ErrorData } from '@/components/info/InfoError'; -import { IUserProfile, IUserUpdateData } from '@/models/user'; -import { contextOutsideScope } from '@/utils/labels'; - -interface IUserProfileContext { - user: IUserProfile | undefined; - loading: boolean; - processing: boolean; - error: ErrorData; - errorProcessing: ErrorData; - setError: (error: ErrorData) => void; - updateUser: (data: IUserUpdateData, callback?: DataCallback) => void; -} - -const ProfileContext = createContext(null); - -export const useUserProfile = () => { - const context = useContext(ProfileContext); - if (!context) { - throw new Error(contextOutsideScope('useUserProfile', 'UserProfileState')); - } - return context; -}; - -export const UserProfileState = ({ children }: React.PropsWithChildren) => { - const [user, setUser] = useState(undefined); - const [loading, setLoading] = useState(true); - const [processing, setProcessing] = useState(false); - const [error, setError] = useState(undefined); - const [errorProcessing, setErrorProcessing] = useState(undefined); - - const reload = useCallback(() => { - setError(undefined); - setUser(undefined); - getProfile({ - showError: true, - setLoading: setLoading, - onError: setError, - onSuccess: newData => setUser(newData) - }); - }, [setUser]); - - const updateUser = useCallback( - (data: IUserUpdateData, callback?: DataCallback) => { - setErrorProcessing(undefined); - patchProfile({ - data: data, - showError: true, - setLoading: setProcessing, - onError: setErrorProcessing, - onSuccess: newData => { - setUser(newData); - // TODO: reload users / optimistic update - callback?.(newData); - } - }); - }, - [setUser] - ); - - useEffect(() => { - reload(); - }, [reload]); - - return ( - - {children} - - ); -}; diff --git a/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx b/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx index 5f8c20f4..20b621f9 100644 --- a/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx @@ -3,12 +3,12 @@ import clsx from 'clsx'; import { useState } from 'react'; +import { useAuth } from '@/backend/auth/useAuth'; import SelectLocationContext from '@/components/select/SelectLocationContext'; import SelectLocationHead from '@/components/select/SelectLocationHead'; import Label from '@/components/ui/Label'; import Modal from '@/components/ui/Modal'; import TextArea from '@/components/ui/TextArea'; -import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import { LocationHead } from '@/models/library'; import { combineLocation, validateLocation } from '@/models/libraryAPI'; diff --git a/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx b/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx index 6690c6dd..c8107adf 100644 --- a/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx +++ b/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; import { VisibilityIcon } from '@/components/DomainIcons'; import SelectAccessPolicy from '@/components/select/SelectAccessPolicy'; import SelectLocationContext from '@/components/select/SelectLocationContext'; @@ -15,7 +16,6 @@ import MiniButton from '@/components/ui/MiniButton'; import Modal from '@/components/ui/Modal'; import TextArea from '@/components/ui/TextArea'; import TextInput from '@/components/ui/TextInput'; -import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library'; diff --git a/rsconcept/frontend/src/hooks/useOssDetails.ts b/rsconcept/frontend/src/hooks/useOssDetails.ts index f9d0b71a..44be1ecf 100644 --- a/rsconcept/frontend/src/hooks/useOssDetails.ts +++ b/rsconcept/frontend/src/hooks/useOssDetails.ts @@ -2,15 +2,15 @@ import { useCallback, useEffect, useState } from 'react'; +import { useAuth } from '@/backend/auth/useAuth'; import { getOssDetails } from '@/backend/oss'; import { type ErrorData } from '@/components/info/InfoError'; -import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { OssLoader } from '@/models/OssLoader'; function useOssDetails({ target }: { target?: string }) { - const { loading: userLoading } = useAuth(); + const { isLoading: userLoading } = useAuth(); const library = useLibrary(); const [schema, setInner] = useState(undefined); const [loading, setLoading] = useState(target != undefined); diff --git a/rsconcept/frontend/src/hooks/useRSFormDetails.ts b/rsconcept/frontend/src/hooks/useRSFormDetails.ts index 34308211..603acfef 100644 --- a/rsconcept/frontend/src/hooks/useRSFormDetails.ts +++ b/rsconcept/frontend/src/hooks/useRSFormDetails.ts @@ -2,14 +2,14 @@ import { useCallback, useEffect, useState } from 'react'; +import { useAuth } from '@/backend/auth/useAuth'; import { getRSFormDetails } from '@/backend/rsforms'; import { type ErrorData } from '@/components/info/InfoError'; -import { useAuth } from '@/context/AuthContext'; import { IRSForm, IRSFormData } from '@/models/rsform'; import { RSFormLoader } from '@/models/RSFormLoader'; function useRSFormDetails({ target, version }: { target?: string; version?: string }) { - const { loading: userLoading } = useAuth(); + const { isLoading: userLoading } = useAuth(); const [schema, setInnerSchema] = useState(undefined); const [loading, setLoading] = useState(target != undefined); const [error, setError] = useState(undefined); diff --git a/rsconcept/frontend/src/models/user.ts b/rsconcept/frontend/src/models/user.ts index f7e9b7c2..08fd9b03 100644 --- a/rsconcept/frontend/src/models/user.ts +++ b/rsconcept/frontend/src/models/user.ts @@ -26,35 +26,9 @@ export interface IUser { * Represents CurrentUser information. */ export interface ICurrentUser extends Pick { - subscriptions: LibraryItemID[]; editor: LibraryItemID[]; } -/** - * Represents login data, used to authenticate users. - */ -export interface IUserLoginData extends Pick { - password: string; -} - -/** - * Represents password reset data. - */ -export interface IResetPasswordData { - password: string; - token: string; -} - -/** - * Represents password token data. - */ -export interface IPasswordTokenData extends Pick {} - -/** - * Represents password reset request data. - */ -export interface IRequestPasswordData extends Pick {} - /** * Represents signup data, used to create new users. */ @@ -63,11 +37,6 @@ export interface IUserSignupData extends Omit { password2: string; } -/** - * Represents user data, intended to update user profile in persistent storage. - */ -export interface IUserUpdateData extends Omit {} - /** * Represents user profile for viewing and editing {@link IUser}. */ @@ -78,14 +47,6 @@ export interface IUserProfile extends Omit {} */ export interface IUserInfo extends Omit {} -/** - * Represents data needed to update password for current user. - */ -export interface IUserUpdatePassword { - old_password: string; - new_password: string; -} - /** * Represents target {@link User}. */ diff --git a/rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx b/rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx index 9b9d3f77..b5b712fc 100644 --- a/rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx +++ b/rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; import { VisibilityIcon } from '@/components/DomainIcons'; import { IconDownload } from '@/components/Icons'; import InfoError from '@/components/info/InfoError'; @@ -19,7 +20,6 @@ import Overlay from '@/components/ui/Overlay'; import SubmitButton from '@/components/ui/SubmitButton'; import TextArea from '@/components/ui/TextArea'; import TextInput from '@/components/ui/TextInput'; -import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; diff --git a/rsconcept/frontend/src/pages/HomePage.tsx b/rsconcept/frontend/src/pages/HomePage.tsx index 9d4a3f84..102da9e3 100644 --- a/rsconcept/frontend/src/pages/HomePage.tsx +++ b/rsconcept/frontend/src/pages/HomePage.tsx @@ -1,17 +1,17 @@ import { useEffect } from 'react'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; import Loader from '@/components/ui/Loader'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { PARAMETER } from '@/utils/constants'; function HomePage() { const router = useConceptNavigation(); - const { user, loading } = useAuth(); + const { user, isLoading } = useAuth(); useEffect(() => { - if (!loading) { + if (!isLoading) { if (!user) { setTimeout(() => { router.replace(urls.manuals); @@ -22,7 +22,7 @@ function HomePage() { }, PARAMETER.refreshTimeout); } } - }, [router, user, loading]); + }, [router, user, isLoading]); return ; } diff --git a/rsconcept/frontend/src/pages/LibraryPage/ViewSideLocation.tsx b/rsconcept/frontend/src/pages/LibraryPage/ViewSideLocation.tsx index 24d7ca21..0b4dbe1b 100644 --- a/rsconcept/frontend/src/pages/LibraryPage/ViewSideLocation.tsx +++ b/rsconcept/frontend/src/pages/LibraryPage/ViewSideLocation.tsx @@ -1,13 +1,13 @@ import clsx from 'clsx'; import { toast } from 'react-toastify'; +import { useAuth } from '@/backend/auth/useAuth'; import { SubfoldersIcon } from '@/components/DomainIcons'; import { IconFolderEdit, IconFolderTree } from '@/components/Icons'; import BadgeHelp from '@/components/info/BadgeHelp'; import { CProps } from '@/components/props'; import SelectLocation from '@/components/select/SelectLocation'; import MiniButton from '@/components/ui/MiniButton'; -import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import useWindowSize from '@/hooks/useWindowSize'; import { FolderNode, FolderTree } from '@/models/FolderTree'; diff --git a/rsconcept/frontend/src/pages/LoginPage.tsx b/rsconcept/frontend/src/pages/LoginPage.tsx index ffff58cb..c79e2b7e 100644 --- a/rsconcept/frontend/src/pages/LoginPage.tsx +++ b/rsconcept/frontend/src/pages/LoginPage.tsx @@ -5,15 +5,15 @@ import clsx from 'clsx'; import { useEffect, useState } from 'react'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; +import { useLogin } from '@/backend/auth/useLogin'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; import TextURL from '@/components/ui/TextURL'; import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import useQueryStrings from '@/hooks/useQueryStrings'; -import { IUserLoginData } from '@/models/user'; import { resources } from '@/utils/constants'; function LoginPage() { @@ -21,23 +21,20 @@ function LoginPage() { const query = useQueryStrings(); const userQuery = query.get('username'); - const { user, login, loading, error, setError } = useAuth(); + const { user } = useAuth(); + const { login, isPending, error, reset } = useLogin(); const [username, setUsername] = useState(userQuery || ''); const [password, setPassword] = useState(''); useEffect(() => { - setError(undefined); - }, [username, password, setError]); + reset(); + }, [username, password, reset]); function handleSubmit(event: React.FormEvent) { event.preventDefault(); - if (!loading) { - const data: IUserLoginData = { - username: username, - password: password - }; - login(data, () => { + if (!isPending) { + login(username, password, () => { if (router.canBack()) { router.back(); } else { @@ -78,7 +75,7 @@ function LoginPage() {
diff --git a/rsconcept/frontend/src/pages/OssPage/MenuOssTabs.tsx b/rsconcept/frontend/src/pages/OssPage/MenuOssTabs.tsx index a96894f5..a40d349b 100644 --- a/rsconcept/frontend/src/pages/OssPage/MenuOssTabs.tsx +++ b/rsconcept/frontend/src/pages/OssPage/MenuOssTabs.tsx @@ -1,6 +1,7 @@ 'use client'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; import { IconAdmin, IconAlert, @@ -19,7 +20,6 @@ import Button from '@/components/ui/Button'; import Divider from '@/components/ui/Divider'; import Dropdown from '@/components/ui/Dropdown'; import DropdownButton from '@/components/ui/DropdownButton'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { useOSS } from '@/context/OssContext'; import useDropdown from '@/hooks/useDropdown'; diff --git a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx index 4289e853..ab52702d 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx @@ -4,7 +4,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; -import { useAuth } from '@/context/AuthContext'; +import { useAuth } from '@/backend/auth/useAuth'; import { useLibrary } from '@/context/LibraryContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { useOSS } from '@/context/OssContext'; diff --git a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx index ef1c0e67..b96ae033 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx @@ -7,12 +7,12 @@ import { TabList, TabPanel, Tabs } from 'react-tabs'; import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import Loader from '@/components/ui/Loader'; import Overlay from '@/components/ui/Overlay'; import TabLabel from '@/components/ui/TabLabel'; import TextURL from '@/components/ui/TextURL'; -import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext'; import { useOSS } from '@/context/OssContext'; @@ -46,7 +46,7 @@ function OssTabs() { useBlockNavigation( isModified && schema !== undefined && - user !== undefined && + !!user && (user.is_staff || user.id == schema.owner || schema.editors.includes(user.id)) ); diff --git a/rsconcept/frontend/src/pages/PasswordChangePage.tsx b/rsconcept/frontend/src/pages/PasswordChangePage.tsx index 070de72b..5c4c2e5a 100644 --- a/rsconcept/frontend/src/pages/PasswordChangePage.tsx +++ b/rsconcept/frontend/src/pages/PasswordChangePage.tsx @@ -5,20 +5,20 @@ import clsx from 'clsx'; import { useEffect, useState } from 'react'; import { urls } from '@/app/urls'; +import { IPasswordTokenDTO, IResetPasswordDTO } from '@/backend/auth/api'; +import { useResetPassword } from '@/backend/auth/useResetPassword'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; import DataLoader from '@/components/wrap/DataLoader'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import useQueryStrings from '@/hooks/useQueryStrings'; -import { IPasswordTokenData, IResetPasswordData } from '@/models/user'; function PasswordChangePage() { const router = useConceptNavigation(); const token = useQueryStrings().get('token'); - const { validateToken, resetPassword, loading, error, setError } = useAuth(); + const { validateToken, resetPassword, isPending, error, reset } = useResetPassword(); const [isTokenValid, setIsTokenValid] = useState(false); const [newPassword, setNewPassword] = useState(''); @@ -31,8 +31,8 @@ function PasswordChangePage() { function handleSubmit(event: React.FormEvent) { event.preventDefault(); - if (!loading) { - const data: IResetPasswordData = { + if (!isPending) { + const data: IResetPasswordDTO = { password: newPassword, token: token! }; @@ -44,21 +44,18 @@ function PasswordChangePage() { } useEffect(() => { - setError(undefined); - }, [newPassword, newPasswordRepeat, setError]); + reset(); + }, [newPassword, newPasswordRepeat, reset]); useEffect(() => { - const data: IPasswordTokenData = { + const data: IPasswordTokenDTO = { token: token ?? '' }; validateToken(data, () => setIsTokenValid(true)); }, [token, validateToken]); - if (error) { - return ; - } return ( - +
{error ? : null} diff --git a/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx index 2ca63664..cb754c13 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx @@ -1,6 +1,7 @@ 'use client'; import { urls } from '@/app/urls'; +import { useAuth } from '@/backend/auth/useAuth'; import { IconAdmin, IconAlert, @@ -31,7 +32,6 @@ import Button from '@/components/ui/Button'; import Divider from '@/components/ui/Divider'; import Dropdown from '@/components/ui/Dropdown'; import DropdownButton from '@/components/ui/DropdownButton'; -import { useAuth } from '@/context/AuthContext'; import { useGlobalOss } from '@/context/GlobalOssContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { useRSForm } from '@/context/RSFormContext'; diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx index 530ab7c0..8e8adbc8 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx @@ -5,7 +5,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; -import { useAuth } from '@/context/AuthContext'; +import { useAuth } from '@/backend/auth/useAuth'; import { useConceptNavigation } from '@/context/NavigationContext'; import { useRSForm } from '@/context/RSFormContext'; import { diff --git a/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx b/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx index 3948bd9c..1e0cc1d1 100644 --- a/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx +++ b/rsconcept/frontend/src/pages/RegisterPage/FormSignup.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; +import { useSignup } from '@/backend/users/useSignup'; import { IconHelp } from '@/components/Icons'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import Button from '@/components/ui/Button'; @@ -17,15 +18,15 @@ import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; import TextURL from '@/components/ui/TextURL'; import Tooltip from '@/components/ui/Tooltip'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { HelpTopic } from '@/models/miscellaneous'; import { IUserSignupData } from '@/models/user'; import { globals, patterns } from '@/utils/constants'; +import { information } from '@/utils/labels'; function FormSignup() { const router = useConceptNavigation(); - const { signup, loading, error, setError } = useAuth(); + const { signup, isPending, error, reset } = useSignup(); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); @@ -40,8 +41,8 @@ function FormSignup() { const isValid = acceptPrivacy && acceptRules && !!email && !!username; useEffect(() => { - setError(undefined); - }, [username, email, password, password2, setError]); + reset(); + }, [username, email, password, password2, reset]); function handleCancel() { if (router.canBack()) { @@ -53,7 +54,7 @@ function FormSignup() { function handleSubmit(event: React.FormEvent) { event.preventDefault(); - if (!loading) { + if (!isPending) { const data: IUserSignupData = { username, email, @@ -64,7 +65,7 @@ function FormSignup() { }; signup(data, createdUser => { router.push(urls.login_hint(createdUser.username)); - toast.success(`Пользователь успешно создан: ${createdUser.username}`); + toast.success(information.newUser(createdUser.username)); }); } } @@ -159,7 +160,7 @@ function FormSignup() {
- +
{error ? : null} diff --git a/rsconcept/frontend/src/pages/RegisterPage/RegisterPage.tsx b/rsconcept/frontend/src/pages/RegisterPage/RegisterPage.tsx index 2f8a1307..bb5a0bb2 100644 --- a/rsconcept/frontend/src/pages/RegisterPage/RegisterPage.tsx +++ b/rsconcept/frontend/src/pages/RegisterPage/RegisterPage.tsx @@ -1,13 +1,13 @@ +import { useAuth } from '@/backend/auth/useAuth'; import Loader from '@/components/ui/Loader'; import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous'; -import { useAuth } from '@/context/AuthContext'; import FormSignup from './FormSignup'; function RegisterPage() { - const { user, loading } = useAuth(); + const { user, isLoading } = useAuth(); - if (loading) { + if (isLoading) { return ; } if (user) { diff --git a/rsconcept/frontend/src/pages/RestorePasswordPage.tsx b/rsconcept/frontend/src/pages/RestorePasswordPage.tsx index 67a3735f..161e4549 100644 --- a/rsconcept/frontend/src/pages/RestorePasswordPage.tsx +++ b/rsconcept/frontend/src/pages/RestorePasswordPage.tsx @@ -4,32 +4,28 @@ import axios from 'axios'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; +import { useRequestPasswordReset } from '@/backend/auth/useRequestPasswordReset'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; import TextURL from '@/components/ui/TextURL'; -import { useAuth } from '@/context/AuthContext'; -import { IRequestPasswordData } from '@/models/user'; function RestorePasswordPage() { - const { requestPasswordReset, loading, error, setError } = useAuth(); + const { requestPasswordReset, isPending, error, reset } = useRequestPasswordReset(); const [isCompleted, setIsCompleted] = useState(false); const [email, setEmail] = useState(''); function handleSubmit(event: React.FormEvent) { event.preventDefault(); - if (!loading) { - const data: IRequestPasswordData = { - email: email - }; - requestPasswordReset(data, () => setIsCompleted(true)); + if (!isPending) { + requestPasswordReset({ email: email }, () => setIsCompleted(true)); } } useEffect(() => { - setError(undefined); - }, [email, setError]); + reset(); + }, [email, reset]); if (isCompleted) { return ( @@ -55,7 +51,7 @@ function RestorePasswordPage() { {error ? : null} diff --git a/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx b/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx index fd6960f4..c5330aa8 100644 --- a/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx +++ b/rsconcept/frontend/src/pages/UserProfilePage/EditorPassword.tsx @@ -6,18 +6,18 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { urls } from '@/app/urls'; +import { IChangePasswordDTO } from '@/backend/auth/api'; +import { useChangePassword } from '@/backend/auth/useChangePassword'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import FlexColumn from '@/components/ui/FlexColumn'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; -import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; -import { IUserUpdatePassword } from '@/models/user'; import { errors, information } from '@/utils/labels'; function EditorPassword() { const router = useConceptNavigation(); - const { updatePassword, error, setError, loading } = useAuth(); + const { changePassword, isPending, error, reset } = useChangePassword(); const [oldPassword, setOldPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); @@ -34,19 +34,19 @@ function EditorPassword() { toast.error(errors.passwordsMismatch); return; } - const data: IUserUpdatePassword = { + const data: IChangePasswordDTO = { old_password: oldPassword, new_password: newPassword }; - updatePassword(data, () => { + changePassword(data, () => { toast.success(information.changesSaved); router.push(urls.login); }); } useEffect(() => { - setError(undefined); - }, [newPassword, oldPassword, newPasswordRepeat, setError]); + reset(); + }, [newPassword, oldPassword, newPasswordRepeat, reset]); return ( {error ? : null} - + ); } diff --git a/rsconcept/frontend/src/pages/UserProfilePage/EditorProfile.tsx b/rsconcept/frontend/src/pages/UserProfilePage/EditorProfile.tsx index 3ff1f69c..22a08c4b 100644 --- a/rsconcept/frontend/src/pages/UserProfilePage/EditorProfile.tsx +++ b/rsconcept/frontend/src/pages/UserProfilePage/EditorProfile.tsx @@ -4,45 +4,44 @@ import axios from 'axios'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; +import { IUpdateProfileDTO } from '@/backend/users/api'; +import { useProfileSuspense } from '@/backend/users/useProfile'; +import { useUpdateProfile } from '@/backend/users/useUpdateProfile'; import InfoError, { ErrorData } from '@/components/info/InfoError'; import SubmitButton from '@/components/ui/SubmitButton'; import TextInput from '@/components/ui/TextInput'; import { useBlockNavigation } from '@/context/NavigationContext'; -import { useUserProfile } from '@/context/UserProfileContext'; -import { IUserUpdateData } from '@/models/user'; import { information } from '@/utils/labels'; function EditorProfile() { - const { updateUser, user, errorProcessing, processing } = useUserProfile(); + const { profile } = useProfileSuspense(); + const { updateProfile, isPending, error } = useUpdateProfile(); - const [username, setUsername] = useState(user?.username ?? ''); - const [email, setEmail] = useState(user?.email ?? ''); - const [first_name, setFirstName] = useState(user?.first_name ?? ''); - const [last_name, setLastName] = useState(user?.last_name ?? ''); + const [username, setUsername] = useState(profile.username); + const [email, setEmail] = useState(profile.email); + const [first_name, setFirstName] = useState(profile.first_name); + const [last_name, setLastName] = useState(profile.last_name); - const isModified = - user != undefined && (user.email !== email || user.first_name !== first_name || user.last_name !== last_name); + const isModified = profile.email !== email || profile.first_name !== first_name || profile.last_name !== last_name; useBlockNavigation(isModified); useEffect(() => { - if (user) { - setUsername(user.username); - setEmail(user.email); - setFirstName(user.first_name); - setLastName(user.last_name); - } - }, [user]); + setUsername(profile.username); + setEmail(profile.email); + setFirstName(profile.first_name); + setLastName(profile.last_name); + }, [profile]); function handleSubmit(event: React.FormEvent) { event.preventDefault(); - const data: IUserUpdateData = { + const data: IUpdateProfileDTO = { username: username, email: email, first_name: first_name, last_name: last_name }; - updateUser(data, () => toast.success(information.changesSaved)); + updateProfile(data, () => toast.success(information.changesSaved)); } return ( @@ -79,11 +78,11 @@ function EditorProfile() { value={email} onChange={event => setEmail(event.target.value)} /> - {errorProcessing ? : null} + {error ? : null} diff --git a/rsconcept/frontend/src/pages/UserProfilePage/UserContents.tsx b/rsconcept/frontend/src/pages/UserProfilePage/UserContents.tsx deleted file mode 100644 index 83c82651..00000000 --- a/rsconcept/frontend/src/pages/UserProfilePage/UserContents.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client'; - -import DataLoader from '@/components/wrap/DataLoader'; -import { useUserProfile } from '@/context/UserProfileContext'; - -import EditorPassword from './EditorPassword'; -import EditorProfile from './EditorProfile'; - -function UserContents() { - const { user, error, loading } = useUserProfile(); - - return ( - -
-

Учетные данные пользователя

-
- - -
-
-
- ); -} - -export default UserContents; diff --git a/rsconcept/frontend/src/pages/UserProfilePage/UserProfilePage.tsx b/rsconcept/frontend/src/pages/UserProfilePage/UserProfilePage.tsx index 6f8f608e..bd63b0a1 100644 --- a/rsconcept/frontend/src/pages/UserProfilePage/UserProfilePage.tsx +++ b/rsconcept/frontend/src/pages/UserProfilePage/UserProfilePage.tsx @@ -1,14 +1,23 @@ -import RequireAuth from '@/components/wrap/RequireAuth'; -import { UserProfileState } from '@/context/UserProfileContext'; +import { Suspense } from 'react'; -import UserContents from './UserContents'; +import Loader from '@/components/ui/Loader'; +import RequireAuth from '@/components/wrap/RequireAuth'; + +import EditorPassword from './EditorPassword'; +import EditorProfile from './EditorProfile'; function UserProfilePage() { return ( - - - + }> +
+

Учетные данные пользователя

+
+ + +
+
+
); } diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index b39c3cce..88424ab3 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -958,8 +958,9 @@ export const information = { noDataToExport: 'Нет данных для экспорта', substitutionsCorrect: 'Таблица отождествлений прошла проверку', - addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, newLibraryItem: 'Схема успешно создана', + addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, + newUser: (username: string) => `Пользователь успешно создан: ${username}`, newVersion: (version: string) => `Версия создана: ${version}`, newConstituent: (alias: string) => `Конституента добавлена: ${alias}`, newOperation: (alias: string) => `Операция добавлена: ${alias}`,